diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8da3de83f87ee327e84aa41c2fcb00753acb9f1..c23a7a3bf0ecd5615d8282a557349c1cdec2b2d1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,7 @@ before_script: spec:feature: script: + - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature tags: - ruby diff --git a/.rubocop.yml b/.rubocop.yml index b4ca11c83439517a461decd5de71dc98c88fd8c5..89aa0591c3135ce26db65e2ef769c74b2b0812ff 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -76,7 +76,7 @@ Style/BlockEndNewline: Description: 'Put end statement of multiline block on its own line.' Enabled: true -Style/Blocks: +Style/BlockDelimiters: Description: >- Avoid using {...} for multi-line blocks (multiline chaining is always ugly). @@ -232,6 +232,10 @@ Style/EvenOdd: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' Enabled: false +Style/ExtraSpacing: + Description: 'Do not use unnecessary spacing.' + Enabled: false + Style/FileName: Description: 'Use snake_case for source file names.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' @@ -431,6 +435,14 @@ Style/OpMethod: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' Enabled: false +Style/ParallelAssignment: + Description: >- + Check for simple usages of parallel assignment. + It will only warn when the number of variables + matches on both sides of the assignment. + StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' + Enabled: false + Style/ParenthesesAroundCondition: Description: >- Don't use parentheses around the condition of an @@ -669,6 +681,13 @@ Style/TrailingWhitespace: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' Enabled: false +Style/TrailingUnderscoreVariable: + Description: >- + Checks for the usage of unneeded trailing underscores at the + end of parallel variable assignment. + AllowNamedUnderscoreVariables: true + Enabled: false + Style/TrivialAccessors: Description: 'Prefer attr_* methods to trivial readers/writers.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' @@ -690,11 +709,6 @@ Style/UnneededPercentQ: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' Enabled: false -Style/UnneededPercentX: - Description: 'Checks for %x when `` would do.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' - Enabled: false - Style/VariableInterpolation: Description: >- Don't interpolate global, instance and class variables @@ -778,6 +792,10 @@ Metrics/MethodLength: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' Enabled: false +Metrics/ModuleLength: + Description: 'Avoid modules longer than 100 lines of code.' + Enabled: false + #################### Lint ################################ ### Warnings @@ -961,6 +979,12 @@ Rails/ActionFilter: Description: 'Enforces consistent use of action filter methods.' Enabled: true +Rails/Date: + Description: >- + Checks the correct usage of date aware methods, + such as Date.today, Date.current etc. + Enabled: false + Rails/DefaultScope: Description: 'Checks if the argument passed to default_scope is a block.' Enabled: false @@ -987,6 +1011,12 @@ Rails/ScopeArgs: Description: 'Checks the arguments of ActiveRecord scopes.' Enabled: false +Rails/TimeZone: + Description: 'Checks the correct usage of time zone aware methods.' + StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' + Reference: 'http://danilenko.org/2012/7/6/rails_timezones' + Enabled: false + Rails/Validation: Description: 'Use validates :attribute, hash of validations.' Enabled: false diff --git a/CHANGELOG b/CHANGELOG index 7f9dfd98cd7a5659511e8869f0da9ea4938bd2c0..ea403642e322afaa83330de79e0f4a1d50980a36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,43 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.3.0 (unreleased) +v 8.4.0 (unreleased) + - Implement new UI for group page + - Implement search inside emoji picker + - Add API support for looking up a user by username (Stan Hu) + - Add project permissions to all project API endpoints (Stan Hu) + - Only allow group/project members to mention `@all` + - Expose Git's version in the admin area (Trey Davis) + - Add "Frequently used" category to emoji picker + - Add CAS support (tduehr) + - Add link to merge request on build detail page + - Revert back upvote and downvote button to the issue and MR pages + - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg) + +v 8.3.3 (unreleased) + - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu) + - Enable "Add key" button when user fills in a proper key (Stan Hu) + +v 8.3.2 + - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu) + - Add support for Google reCAPTCHA in user registration + +v 8.3.1 + - Fix Error 500 when global milestones have slashes (Stan Hu) + - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu) + - Fix LDAP identity and user retrieval when special characters are used + - Move Sidekiq-cron configuration to gitlab.yml + - Enable forcing Two-Factor authentication sitewide, with optional grace period + +v 8.3.0 + - Bump rack-attack to 4.3.1 for security fix (Stan Hu) + - API support for starred projects for authorized user (Zeger-Jan van de Weg) + - Add open_issues_count to project API (Stan Hu) - Expand character set of usernames created by Omniauth (Corey Hinshaw) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - - Merge when build succeeds (Zeger-Jan van de Weg) - Provide better diagnostic message upon project creation errors (Stan Hu) - Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu) + - Remove api credentials from link to build_page + - Deprecate GitLabCiService making it to always be inactive - Bump gollum-lib to 4.1.0 (Stan Hu) - Fix broken group avatar upload under "New group" (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu) @@ -17,13 +49,16 @@ v 8.3.0 (unreleased) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) - Recognize issue/MR/snippet/commit links as references + - Backport JIRA features from EE to CE - Add ignore whitespace change option to commit view - Fire update hook from GitLab + - Allow account unlock via email - Style warning about mentioning many people in a comment - Fix: sort milestones by due date once again (Greg Smethells) - Migrate all CI::Services and CI::WebHooks to Services and WebHooks - Don't show project fork event as "imported" - Add API endpoint to fetch merge request commits list + - Don't create CI status for refs that doesn't have .gitlab-ci.yml, even if the builds are enabled - Expose events API with comment information and author info - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583 - Run custom Git hooks when branch is created or deleted. @@ -62,8 +97,6 @@ v 8.2.3 - Update documentation for "Guest" permissions - Properly convert Emoji-only comments into Award Emojis - Enable devise paranoid mode to prevent user enumeration attack - -v 8.2.3 - Webhook payload has an added, modified and removed properties for each commit - Fix 500 error when creating a merge request that removes a submodule diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ced7c57889557f6da23e60a3a7c7e29fdfa3000..b9c2b3d2f8e8242009998736c80c1303a90a2c9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,6 +155,28 @@ sudo -u git -H bundle exec rake gitlab:env:info) ``` +### Issue weight + +Issue weight allows us to get an idea of the amount of work required to solve +one or multiple issues. This makes it possible to schedule work more accurately. + +You are encouraged to set the weight of any issue. Following the guidelines +below will make it easy to manage this, without unnecessary overhead. + +1. Set weight for any issue at the earliest possible convenience +1. If you don't agree with a set weight, discuss with other developers until +consensus is reached about the weight +1. Issue weights are an abstract measurement of complexity of the issue. Do not +relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring) +and something you want to avoid. +1. Something that has a weight of 1 (or no weight) is really small and simple. +Something that is 9 is rewriting a large fundamental part of GitLab, +which might lead to many hard problems to solve. Changing some text in GitLab +is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9. +1. If something is very large, it should probably be split up in multiple +issues or chunks. You can simply not set the weight of a parent issue and set +weights to children issues. + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, @@ -358,7 +380,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [core team]: https://about.gitlab.com/core-team/ [getting help page]: https://about.gitlab.com/getting-help/ [Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq -[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up+for+grabs +[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs [medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455 [ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues [ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 743af5e1251c4fc79e10f34ef7c9f24b1367ab72..d48d3702aed9c6d03de90d13d12199015ce4740e 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.8 +2.6.9 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 2b7c5ae01848a77d95e2792eb83ab605c9aed91a..4b9fcbec101a6ff8ec68e0f95131ccda4861407f 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.4.2 +0.5.1 diff --git a/Gemfile b/Gemfile index 7298e21ce6634a165299c3badc79d50da785f633..2a1c4f7d73a0b2de1fb10c4c153772f4c2148ef4 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' @@ -34,6 +35,9 @@ gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd' gem 'rack-oauth2', '~> 1.2.1' +# reCAPTCHA protection +gem 'recaptcha', require: 'recaptcha/rails' + # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' gem 'rqrcode-rails3', '~> 0.1.7' @@ -101,6 +105,9 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' gem 'rouge', '~> 1.10.1' +# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s +gem 'nokogiri', '1.6.7.1' + # Diffs gem 'diffy', '~> 3.0.3' @@ -168,14 +175,14 @@ gem 'd3_rails', '~> 3.5.5' gem "cal-heatmap-rails", "~> 0.0.1" # underscore-rails -gem "underscore-rails", "~> 1.4.4" +gem "underscore-rails", "~> 1.8.0" # Sanitize user input gem "sanitize", '~> 2.0' gem 'babosa', '~> 1.0.2' # Protect against bruteforcing -gem "rack-attack", '~> 4.3.0' +gem "rack-attack", '~> 4.3.1' # Ace editor gem 'ace-rails-ap', '~> 2.0.1' @@ -186,7 +193,7 @@ gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding gem 'charlock_holmes', '~> 0.7.3' -gem "sass-rails", '~> 4.0.5' +gem "sass-rails", '~> 5.0.0' gem "coffee-rails", '~> 4.1.0' gem "uglifier", '~> 2.7.2' gem 'turbolinks', '~> 2.5.0' @@ -198,9 +205,9 @@ gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.2.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' -gem 'jquery-rails', '~> 3.1.3' +gem 'jquery-rails', '~> 4.0.0' gem 'jquery-scrollto-rails', '~> 1.4.3' -gem 'jquery-ui-rails', '~> 4.2.1' +gem 'jquery-ui-rails', '~> 5.0.0' gem 'nprogress-rails', '~> 0.1.6.7' gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.2.0' @@ -208,14 +215,22 @@ gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +# Metrics +group :metrics do + gem 'allocations', '~> 1.0', require: false, platform: :mri + gem 'method_source', '~> 0.8', require: false + gem 'influxdb', '~> 0.2', require: false + gem 'connection_pool', '~> 2.0', require: false +end + group :development do gem "foreman" - gem 'brakeman', '3.0.1', require: false + gem 'brakeman', '~> 3.1.0', require: false gem "annotate", "~> 2.6.0" gem "letter_opener", '~> 1.1.2' gem 'quiet_assets', '~> 1.0.2' - gem 'rerun', '~> 0.10.0' + gem 'rerun', '~> 0.11.0' gem 'bullet', require: false gem 'rblineprof', platform: :mri, require: false gem 'web-console', '~> 2.0' @@ -251,7 +266,7 @@ group :development, :test do gem 'capybara', '~> 2.4.0' gem 'capybara-screenshot', '~> 1.0.0' - gem 'poltergeist', '~> 1.6.0' + gem 'poltergeist', '~> 1.8.1' gem 'teaspoon', '~> 1.0.0' gem 'teaspoon-jasmine', '~> 2.2.0' @@ -261,7 +276,7 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.28.0', require: false + gem 'rubocop', '~> 0.35.0', require: false gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.10.0', require: false gem 'flog', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ff57460f5bb5f55e5156c7b1145ac658d23a5d21..9769ae80a7dfa84f11518c1a1f1e954f240e3c3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,6 +49,7 @@ GEM addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) + allocations (1.0.3) annotate (2.6.10) activerecord (>= 3.2, <= 4.3) rake (~> 10.4) @@ -65,7 +66,7 @@ GEM attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (6.1.1) + autoprefixer-rails (6.1.2) execjs json awesome_print (1.2.0) @@ -84,15 +85,17 @@ GEM bootstrap-sass (3.3.5) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) - brakeman (3.0.1) + brakeman (3.1.4) erubis (~> 2.6) fastercsv (~> 1.5) haml (>= 3.0, < 5.0) - highline (~> 1.6.20) + highline (>= 1.6.20, < 2.0) multi_json (~> 1.2) - ruby2ruby (~> 2.1.1) - ruby_parser (~> 3.5.0) + ruby2ruby (>= 2.1.1, < 2.3.0) + ruby_parser (~> 3.7.0) + safe_yaml (>= 1.0) sass (~> 3.0) + slim (>= 1.3.6, < 4.0) terminal-table (~> 1.4) browser (1.0.1) builder (3.2.2) @@ -102,7 +105,7 @@ GEM bundler-audit (0.4.0) bundler (~> 1.2) thor (~> 0.18) - byebug (8.2.0) + byebug (8.2.1) cal-heatmap-rails (0.0.1) capybara (2.4.4) mime-types (>= 1.16) @@ -117,23 +120,7 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.17.2) - celluloid-essentials - celluloid-extras - celluloid-fsm - celluloid-pool - celluloid-supervision - timers (>= 4.1.1) - celluloid-essentials (0.20.5) - timers (>= 4.1.1) - celluloid-extras (0.20.5) - timers (>= 4.1.1) - celluloid-fsm (0.20.5) - timers (>= 4.1.1) - celluloid-pool (0.20.5) - timers (>= 4.1.1) - celluloid-supervision (0.20.5) - timers (>= 4.1.1) + cause (0.1) charlock_holmes (0.7.3) chunky_png (1.3.5) cliver (0.3.2) @@ -157,10 +144,10 @@ GEM term-ansicolor (~> 1.3) thor (~> 0.19.1) tins (~> 1.6.0) - crack (0.4.2) + crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) - d3_rails (3.5.6) + d3_rails (3.5.11) railties (>= 3.1.0) daemons (1.2.3) database_cleaner (1.4.1) @@ -247,7 +234,7 @@ GEM ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) opennebula - fog-brightbox (0.9.0) + fog-brightbox (0.10.1) fog-core (~> 1.22) fog-json inflecto (~> 0.0.2) @@ -266,7 +253,7 @@ GEM fog-core (>= 1.21.0) fog-json fog-xml (>= 0.0.1) - fog-sakuracloud (1.4.0) + fog-sakuracloud (1.5.0) fog-core fog-json fog-softlayer (1.0.2) @@ -294,11 +281,11 @@ GEM ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) - gemojione (2.1.0) + gemojione (2.1.1) json get_process_mem (0.2.0) gherkin-ruby (0.3.2) - github-linguist (4.7.2) + github-linguist (4.7.3) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) mime-types (>= 1.19) @@ -315,7 +302,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.2.0) gemojione (~> 2.1) - gitlab_git (7.2.21) + gitlab_git (7.2.22) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -364,12 +351,11 @@ GEM html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (3.4.3) - highline (1.6.21) + highline (1.7.8) hike (1.2.3) hipchat (1.5.2) httparty mimemagic - hitimes (1.2.3) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) @@ -388,17 +374,21 @@ GEM i18n (0.7.0) ice_nine (0.11.1) inflecto (0.0.2) + influxdb (0.2.3) + cause + json ipaddress (0.8.0) jquery-atwho-rails (1.3.2) - jquery-rails (3.1.4) - railties (>= 3.0, < 5.0) + jquery-rails (4.0.5) + rails-dom-testing (~> 1.0) + railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) railties (> 3.1, < 5.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks - jquery-ui-rails (4.2.1) + jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) jwt (1.5.2) @@ -410,8 +400,7 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.9.0) - celluloid (>= 0.15.2) + listen (3.0.5) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) loofah (2.0.3) @@ -424,7 +413,7 @@ GEM method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) - mini_portile (0.6.2) + mini_portile2 (2.0.0) minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.11.2) @@ -435,12 +424,12 @@ GEM net-ldap (0.12.1) net-ssh (3.0.1) netrc (0.11.0) - newrelic-grape (2.0.0) + newrelic-grape (2.1.0) grape newrelic_rpm newrelic_rpm (3.9.4.245) - nokogiri (1.6.6.4) - mini_portile (~> 0.6.0) + nokogiri (1.6.7.1) + mini_portile2 (~> 2.0.0.rc2) nprogress-rails (0.1.6.7) oauth (0.4.7) oauth2 (1.0.0) @@ -458,6 +447,10 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) + omniauth-cas3 (1.1.3) + addressable (~> 2.3) + nokogiri (~> 1.6.6) + omniauth (~> 1.2) omniauth-facebook (3.0.0) omniauth-oauth2 (~> 1.2) omniauth-github (1.1.2) @@ -507,13 +500,13 @@ GEM parser (2.2.3.0) ast (>= 1.1, < 3.0) pg (0.18.4) - poltergeist (1.6.0) + poltergeist (1.8.1) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) posix-spawn (0.3.11) - powerpack (0.0.9) + powerpack (0.1.1) pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -526,7 +519,7 @@ GEM rack (1.6.4) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.3.0) + rack-attack (4.3.1) rack rack-cors (0.4.0) rack-mount (0.8.3) @@ -580,6 +573,8 @@ GEM trollop rdoc (3.12.2) json (~> 1.4) + recaptcha (1.0.2) + json redcarpet (3.3.3) redis (3.2.2) redis-actionpack (4.0.1) @@ -601,8 +596,8 @@ GEM redis-store (1.1.7) redis (>= 2.2) request_store (1.2.1) - rerun (0.10.0) - listen (~> 2.7, >= 2.7.3) + rerun (0.11.0) + listen (~> 3.0) responders (2.1.0) railties (>= 4.2.0, < 5) rest-client (1.8.0) @@ -637,22 +632,23 @@ GEM rspec-mocks (~> 3.3.0) rspec-support (~> 3.3.0) rspec-support (3.3.0) - rubocop (0.28.0) + rubocop (0.35.1) astrolabe (~> 1.3) - parser (>= 2.2.0.pre.7, < 3.0) - powerpack (~> 0.0.6) + parser (>= 2.2.3.0, < 3.0) + powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.4) + ruby-progressbar (~> 1.7) + tins (<= 1.6.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-progressbar (1.7.5) ruby-saml (1.0.0) nokogiri (>= 1.5.10) uuid (~> 2.3) - ruby2ruby (2.1.4) + ruby2ruby (2.2.0) ruby_parser (~> 3.1) sexp_processor (~> 4.0) - ruby_parser (3.5.0) + ruby_parser (3.7.2) sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) @@ -661,12 +657,13 @@ GEM safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) - sass (3.2.19) - sass-rails (4.0.5) + sass (3.4.20) + sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) - sass (~> 3.2.2) - sprockets (~> 2.8, < 3.0) - sprockets-rails (~> 2.0) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) sawyer (0.6.0) addressable (~> 2.3.5) faraday (~> 0.8, < 0.10) @@ -705,6 +702,9 @@ GEM tilt (>= 1.3, < 3) six (0.2.0) slack-notifier (1.2.1) + slim (3.0.6) + temple (~> 0.7.3) + tilt (>= 1.3.3, < 2.1) slop (3.6.0) spinach (0.8.10) colorize @@ -746,6 +746,7 @@ GEM railties (>= 3.2.5, < 5) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) + temple (0.7.6) term-ansicolor (1.3.2) tins (~> 1.0) terminal-table (1.5.2) @@ -758,8 +759,6 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.1.1) - hitimes timfel-krb5-auth (0.8.3) tinder (1.10.1) eventmachine (~> 1.0) @@ -783,7 +782,7 @@ GEM uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) - underscore-rails (1.4.4) + underscore-rails (1.8.3) unf (0.1.4) unf_ext unf_ext (0.0.7.1) @@ -803,7 +802,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.3) + warden (1.2.4) rack (>= 1.0) web-console (2.2.1) activemodel (>= 4.0) @@ -834,6 +833,7 @@ DEPENDENCIES acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue + allocations (~> 1.0) annotate (~> 2.6.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) @@ -844,7 +844,7 @@ DEPENDENCIES better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.0) - brakeman (= 3.0.1) + brakeman (~> 3.1.0) browser (~> 1.0.0) bullet bundler-audit @@ -856,6 +856,7 @@ DEPENDENCIES charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) colorize (~> 0.7.0) + connection_pool (~> 2.0) coveralls (~> 0.8.2) creole (~> 0.5.0) d3_rails (~> 3.5.5) @@ -893,14 +894,16 @@ DEPENDENCIES hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) + influxdb (~> 0.2) jquery-atwho-rails (~> 1.3.2) - jquery-rails (~> 3.1.3) + jquery-rails (~> 4.0.0) jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) - jquery-ui-rails (~> 4.2.1) + jquery-ui-rails (~> 5.0.0) kaminari (~> 0.16.3) letter_opener (~> 1.1.2) mail_room (~> 0.6.1) + method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) @@ -908,11 +911,13 @@ DEPENDENCIES net-ssh (~> 3.0.1) newrelic-grape newrelic_rpm (~> 3.9.4.245) + nokogiri (= 1.6.7.1) nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) octokit (~> 3.7.0) omniauth (~> 1.2.2) omniauth-bitbucket (~> 0.0.2) + omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) @@ -925,10 +930,10 @@ DEPENDENCIES org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) - poltergeist (~> 1.6.0) + poltergeist (~> 1.8.1) pry-rails quiet_assets (~> 1.0.2) - rack-attack (~> 4.3.0) + rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) rails (= 4.2.4) @@ -936,19 +941,20 @@ DEPENDENCIES raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) + recaptcha redcarpet (~> 3.3.3) redis-namespace redis-rails (~> 4.0.0) request_store (~> 1.2.0) - rerun (~> 0.10.0) + rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) - rubocop (~> 0.28.0) + rubocop (~> 0.35.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) - sass-rails (~> 4.0.5) + sass-rails (~> 5.0.0) sdoc (~> 0.3.20) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) @@ -977,7 +983,7 @@ DEPENDENCIES tinder (~> 1.10.0) turbolinks (~> 2.5.0) uglifier (~> 2.7.2) - underscore-rails (~> 1.4.4) + underscore-rails (~> 1.8.0) unf (~> 0.1.4) unicorn (~> 4.8.2) unicorn-worker-killer (~> 0.4.2) diff --git a/VERSION b/VERSION index 8d0676ff07ba5372063bb36af9529a8d976c16ad..ce669730119df83f8d693ec9e7401baf55b9245a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.0.pre +8.4.0.pre diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png deleted file mode 100644 index 9c564bb61411bf4f0dec3e21aa86848ac70cbcb8..0000000000000000000000000000000000000000 Binary files a/app/assets/images/brand_logo.png and /dev/null differ diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ad7b6eab612af71df106bf8ac26541a92fe217 Binary files /dev/null and b/app/assets/images/emoji.png differ diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0c157546b9cf87d9aaf9a404c01f8a623f7c24c7 Binary files /dev/null and b/app/assets/images/gitlab_logo.png differ diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 1539eba0faa9e0ad0bb136cca26533492669c3e0..affab5bb030d8c72eba0fb7295f700cca0aecf1a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -5,7 +5,7 @@ # the compiled file. # #= require jquery -#= require jquery.ui.all +#= require jquery-ui #= require jquery_ujs #= require jquery.cookie #= require jquery.endless-scroll diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 3ff9ba77dfc7fd77187b66aacfdaf73877f6390b..619abb1fb071367785e20e80b975d8c4199a8af4 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,12 +1,28 @@ class @AwardsHandler constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + $(".add-award").click (event)-> + event.stopPropagation() + event.preventDefault() + $(".emoji-menu").show() + + $("html").click -> + if !$(event.target).closest(".emoji-menu").length + if $(".emoji-menu").is(":visible") + $(".emoji-menu").hide() + + @renderFrequentlyUsedBlock() + @setupSearch() addAward: (emoji) -> emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) + + $(".emoji-menu").hide() - addAwardToEmojiBar: (emoji, custom_path = '') -> + addAwardToEmojiBar: (emoji) -> + @addEmojiToFrequentlyUsedList(emoji) + emoji = @normilizeEmojiName(emoji) if @exist(emoji) if @isActive(emoji) @@ -17,7 +33,7 @@ class @AwardsHandler counter.parent().addClass("active") @addMeToAuthorList(emoji) else - @createEmoji(emoji, custom_path) + @createEmoji(emoji) exist: (emoji) -> @findEmojiIcon(emoji).length > 0 @@ -27,15 +43,19 @@ class @AwardsHandler decrementCounter: (emoji) -> counter = @findEmojiIcon(emoji).siblings(".counter") + emojiIcon = counter.parent() if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) - counter.parent().removeClass("active") + emojiIcon.removeClass("active") @removeMeFromAuthorList(emoji) + else if emoji =="thumbsup" || emoji == "thumbsdown" + emojiIcon.tooltip("destroy") + counter.text(0) + emojiIcon.removeClass("active") else - award = counter.parent() - award.tooltip("destroy") - award.remove() + emojiIcon.tooltip("destroy") + emojiIcon.remove() removeMeFromAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() @@ -54,35 +74,39 @@ class @AwardsHandler resetTooltip: (award) -> award.tooltip("destroy") - # "destroy" call is asynchronous, this is why we need to set timeout. + # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> award.tooltip() ), 200 - createEmoji: (emoji, custom_path) -> + createEmoji: (emoji) -> + emojiCssClass = @resolveNameToCssClass(emoji) + nodes = [] nodes.push("<div class='award active' title='me'>") - nodes.push("<div class='icon' data-emoji='" + emoji + "'>") - nodes.push(@getImage(emoji, custom_path)) + nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") + nodes.push("<div class='counter'>1</div>") nodes.push("</div>") - nodes.push("<div class='counter'>1") - nodes.push("</div></div>") - $(".awards-controls").before(nodes.join("\n")) + emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) $(".award").tooltip() - getImage: (emoji, custom_path) -> - if custom_path - $("<img>").attr({src: custom_path, width: 20, height: 20}).wrap("<div>").parent().html() + resolveNameToCssClass: (emoji) -> + emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") + + if emoji_icon.length > 0 + unicodeName = emoji_icon.data("unicode-name") else - $("li[data-emoji='" + emoji + "']").html() + # Find by alias + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") + "emoji-#{unicodeName}" postEmoji: (emoji, callback) -> $.post @post_emoji_url, { note: { - note: ":" + emoji + ":" + note: ":#{emoji}:" noteable_type: @noteable_type noteable_id: @noteable_id }},(data) -> @@ -90,7 +114,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".icon[data-emoji='" + emoji + "']") + $(".award [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ @@ -99,3 +123,44 @@ class @AwardsHandler normilizeEmojiName: (emoji) -> @aliases[emoji] || emoji + + addEmojiToFrequentlyUsedList: (emoji) -> + frequently_used_emojis = @getFrequentlyUsedEmojis() + frequently_used_emojis.push(emoji) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) + + getFrequentlyUsedEmojis: -> + frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") + _.compact(_.uniq(frequently_used_emojis)) + + renderFrequentlyUsedBlock: -> + if $.cookie('frequently_used_emojis') + frequently_used_emojis = @getFrequentlyUsedEmojis() + + ul = $("<ul>") + + for emoji in frequently_used_emojis + do (emoji) -> + $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + + $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used")) + + setupSearch: -> + $("input.emoji-search").keyup (ev) => + term = $(ev.target).val() + + # Clean previous search results + $("ul.emoji-search,h5.emoji-search").remove() + + if term + # Generate a search result block + h5 = $("<h5>").text("Search results").addClass("emoji-search") + found_emojis = @searchEmojis(term).show() + ul = $("<ul>").addClass("emoji-search").append(found_emojis) + $(".emoji-menu-content ul, .emoji-menu-content h5").hide() + $(".emoji-menu-content").append(h5).append(ul) + else + $(".emoji-menu-content").children().show() + + searchEmojis: (term)-> + $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee index 195f8b11e5dbac896379cd30d13f3caa3532e130..9df932817f6aa785360c5b0a973f3d3895369be7 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -35,7 +35,7 @@ class @BlobFileDropzone return this.on 'sending', (file, xhr, formData) -> - formData.append('new_branch', form.find('.js-new-branch').val()) + formData.append('target_branch', form.find('.js-target-branch').val()) formData.append('create_merge_request', form.find('.js-create-merge-request').val()) formData.append('commit_message', form.find('.js-commit-message').val()) return diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 599b4c4954001823ebd5a7a77a01bac97069b28e..69e061ce6e927813b489eea7eee06ac7c29e8c4a 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -49,7 +49,7 @@ class Dispatcher new DropzoneInput($('.release-form')) when 'projects:merge_requests:show' new Diff() - shortcut_handler = new ShortcutsIssuable() + shortcut_handler = new ShortcutsIssuable(true) new ZenMode() when "projects:merge_requests:diffs" new Diff() diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 01bd515cc0215c43f21b7b7fe2432d6946096807..02232698bc20ea522d4a47c414b691b496092d24 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -18,7 +18,7 @@ class @IssuableContext $('.issuable-affix').affix offset: top: -> - @top = ($('.issuable-affix').offset().top - 60) + @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 603a16da1ce645d51f2958fa68437e91653ebb65..c256ec8f41bb178189c5e39ce1a589d312a05dd8 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,3 +1,4 @@ +#= require flash #= require jquery.waitforimages #= require task_list @@ -6,16 +7,47 @@ class @Issue # Prevent duplicate event bindings @disableTaskList() - if $("a.btn-close").length + if $('a.btn-close').length @initTaskList() + @initIssueBtnEventListeners() initTaskList: -> - $('.issue-details .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList + $('.detail-page-description .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList + + initIssueBtnEventListeners: -> + issueFailMessage = 'Unable to update this issue at this time.' + $('a.btn-close, a.btn-reopen').on 'click', (e) -> + e.preventDefault() + e.stopImmediatePropagation() + $this = $(this) + isClose = $this.hasClass('btn-close') + $this.prop('disabled', true) + url = $this.attr('href') + $.ajax + type: 'PUT' + url: url, + error: (jqXHR, textStatus, errorThrown) -> + issueStatus = if isClose then 'close' else 'open' + new Flash(issueFailMessage, 'alert') + success: (data, textStatus, jqXHR) -> + if data.saved + $this.addClass('hidden') + if isClose + $('a.btn-reopen').removeClass('hidden') + $('div.status-box-closed').removeClass('hidden') + $('div.status-box-open').addClass('hidden') + else + $('a.btn-close').removeClass('hidden') + $('div.status-box-closed').addClass('hidden') + $('div.status-box-open').removeClass('hidden') + else + new Flash(issueFailMessage, 'alert') + $this.prop('disabled', false) disableTaskList: -> - $('.issue-details .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.issue-details .js-task-list-container' + $('.detail-page-description .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' # TODO (rspeicher): Make the issue description inline-editable like a note so # that we can re-use its form here diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index b21cb7904b56fdd99739d81c6458ce556af6b414..9047587db81af96a3aeca6a6451b64851a3cd4ba 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -40,12 +40,12 @@ class @MergeRequest this.$('.all-commits').removeClass 'hide' initTaskList: -> - $('.merge-request-details .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList + $('.detail-page-description .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList disableTaskList: -> - $('.merge-request-details .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container' + $('.detail-page-description .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' # TODO (rspeicher): Make the merge request description inline-editable like a # note so that we can re-use its form here diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index c4b63966fe77e590ce114d5968fe94bafea36db5..738ffc8343bfd34437915c56f7148ec2cbc8f5cc 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -18,7 +18,7 @@ class @MergeRequestWidget if data.state == "merged" urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' - window.location.href = window.location.href + urlSuffix + window.location.href = window.location.pathname + urlSuffix else if data.merge_error $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>") else diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..4b350854f7855d438dc3a06d6c5ce202c9ed91af --- /dev/null +++ b/app/assets/javascripts/new_branch_form.js.coffee @@ -0,0 +1,78 @@ +class @NewBranchForm + constructor: (form, availableRefs) -> + @branchNameError = form.find('.js-branch-name-error') + @name = form.find('.js-branch-name') + @ref = form.find('#ref') + + @setupAvailableRefs(availableRefs) + @setupRestrictions() + @addBinding() + @init() + + addBinding: -> + @name.on 'blur', @validate + + init: -> + @name.trigger 'blur' if @name.val().length > 0 + + setupAvailableRefs: (availableRefs) -> + @ref.autocomplete + source: availableRefs, + minLength: 1 + + setupRestrictions: -> + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" + } + + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" + } + + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g + prefix: "can't contain", + conjunction: ", " + } + + single = { + pattern: /^@+$/g + prefix: "can't be", + conjunction: "or" + } + + @restrictions = [startsWith, invalid, endsWith, single] + + validate: => + @branchNameError.empty() + + unique = (values, value) -> + values.push(value) unless value in values + values + + formatter = (values, restriction) -> + formatted = values.map (value) -> + switch + when /\s/.test value then 'spaces' + when /\/{2,}/g.test value then 'consecutive slashes' + else "'#{value}'" + + "#{restriction.prefix} #{formatted.join(restriction.conjunction)}" + + validator = (errors, restriction) => + matched = @name.val().match(restriction.pattern) + + if matched + errors.concat formatter(matched.reduce(unique, []), restriction) + else + errors + + errors = @restrictions.reduce validator, [] + + if errors.length > 0 + errorMessage = $("<span/>").text(errors.join(', ')) + @branchNameError.append(errorMessage) diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee index 3c7b776155fb2929da92df64237075e0bff6bc52..03f0f51acfad536ba0927ceaf5541f774796e0dd 100644 --- a/app/assets/javascripts/new_commit_form.js.coffee +++ b/app/assets/javascripts/new_commit_form.js.coffee @@ -1,6 +1,6 @@ class @NewCommitForm constructor: (form) -> - @newBranch = form.find('.js-new-branch') + @newBranch = form.find('.js-target-branch') @originalBranch = form.find('.js-original-branch') @createMergeRequest = form.find('.js-create-merge-request') @createMergeRequestContainer = form.find('.js-create-merge-request-container') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 35dc7829da2d95571c8a7b41797709115a206f25..9e5204bfeebb7b474ad935e64a3eb59e46d2daa2 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -127,7 +127,7 @@ class @Notes @initTaskList() if note.award - awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) + awards_handler.addAwardToEmojiBar(note.note) awards_handler.scrollToAwards() ### diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 1f221945c062668955f838add7ab4c034933c681..d7a658f8faa4dd0ce23db69fef1de40d01ec137b 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,7 +1,7 @@ class @Project constructor: -> # Git protocol switcher - $('.js-protocol-switch').click -> + $('ul.clone-options-dropdown a').click -> return if $(@).hasClass('active') @@ -10,7 +10,8 @@ class @Project # Add the active class for the clicked button $(@).toggleClass('active') - url = $(@).data('clone') + url = $("#project_clone").val() + console.log("url",url) # Update the input field $('#project_clone').val(url) diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index db5faf71faf79461144bdd260d9bb3adbf380c0b..f2887af190bc5a9e4093786822c0e51138a881e1 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -8,17 +8,17 @@ class @ProjectsList $(".projects-list-filter").keyup -> terms = $(this).val() - uiBox = $(this).closest('.projects-list-holder') + uiBox = $('div.projects-list-holder') if terms == "" || terms == undefined - uiBox.find(".projects-list li").show() + uiBox.find("ul.projects-list li").show() else - uiBox.find(".projects-list li").each (index) -> - name = $(this).find(".filter-title").text() + uiBox.find("ul.projects-list li").each (index) -> + name = $(this).find("span.filter-title").text() if name.toLowerCase().search(terms.toLowerCase()) == -1 $(this).hide() else $(this).show() - uiBox.find(".projects-list li.bottom").hide() + uiBox.find("ul.projects-list li.bottom").hide() diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index e9aeb1e9525ccd00e733b625178e5b2fcb8fa84a..4d915bfc8c5c3e83b8d2bfd94659050ed2d0edcd 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -7,7 +7,7 @@ class @Shortcuts selectiveHelp: (e) => Shortcuts.showHelp(e, @enabledHelp) - + @showHelp: (e, location) -> if $('#modal-shortcuts').length > 0 $('#modal-shortcuts').modal('show') @@ -17,8 +17,7 @@ class @Shortcuts dataType: 'script', success: (e) -> if location and location.length > 0 - for l in location - $(l).show() + $(l).show() for l in location else $('.hidden-shortcut').show() $('.js-more-help-button').remove() @@ -28,3 +27,8 @@ class @Shortcuts @focusSearch: (e) -> $('#search').focus() e.preventDefault() + +$(document).on 'click.more_help', '.js-more-help-button', (e) -> + $(@).remove() + $('.hidden-shortcut').show() + e.preventDefault() diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..d849b2e79500adbf730f3bee7a2ab1fb0ec18527 --- /dev/null +++ b/app/assets/javascripts/star.js.coffee @@ -0,0 +1,22 @@ +class @Star + constructor: -> + $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) -> + $this = $(this) + $starSpan = $this.find('span') + $starIcon = $this.find('i') + + toggleStar = (isStarred) -> + $this.parent().find('span.count').text data.star_count + if isStarred + $starSpan.removeClass('starred').text 'Star' + $starIcon.removeClass('fa-star').addClass 'fa-star-o' + else + $starSpan.addClass('starred').text 'Unstar' + $starIcon.removeClass('fa-star-o').addClass 'fa-star' + return + + toggleStar $starSpan.hasClass('starred') + return + ).on 'ajax:error', (e, xhr, status, error) -> + new Flash('Star toggle failed. Try again later.', 'alert') + return \ No newline at end of file diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 12abf806bfab11ec03a633b8526c3e1e112f203a..9467011799fd33f1342095fcad80ffdde73cf0fe 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -117,5 +117,5 @@ class @UsersSelect callback(users) buildUrl: (url) -> - url = gon.relative_url_root + url if gon.relative_url_root? + url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root? return url diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 7b060ce48532c36dadd52950c5f8f293a7515821..0c0451fe4ddffcca924242dc13424067609af4a0 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,8 +2,8 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery.ui.datepicker - *= require jquery.ui.autocomplete + *= require jquery-ui/datepicker + *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self @@ -48,4 +48,4 @@ /* * Styles for JS behaviors. */ -@import "behaviors.scss"; \ No newline at end of file +@import "behaviors.scss"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index a62c0f62a4c862cd950accfcf367e17ecc5dfc4c..206d39cc9b3b198f42f82729eb5b1aca18062876 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -76,7 +76,7 @@ .cover-block { text-align: center; - background: #f7f8fa; + background: $background-color; margin: -$gl-padding; margin-bottom: 0; padding: 44px $gl-padding; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index fe56266284b9d5c5709ce5d2cdd0f2d00eb6226e..97a94638847576c5b8695581e3dd83f21c781865 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,10 +1,9 @@ @mixin btn-default { - @include border-radius(2px); + @include border-radius(3px); border-width: 1px; border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; + font-size: 15px; + font-weight: 500; line-height: 18px; padding: 11px $gl-padding; letter-spacing: .4px; @@ -18,7 +17,7 @@ @mixin btn-middle { @include btn-default; - @include border-radius(2px); + @include border-radius(3px); padding: 11px 24px; } @@ -51,6 +50,10 @@ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF); } +@mixin btn-blue-medium { + @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF); +} + @mixin btn-orange { @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF); } @@ -60,7 +63,7 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236); + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236); } @mixin btn-white { @@ -75,6 +78,10 @@ padding: 5px 10px; } + &.btn-nr { + padding: 7px 10px; + } + &.btn-xs { padding: 1px 5px; } @@ -91,11 +98,15 @@ @include btn-gray; } - &.btn-primary, + &.btn-primary { + @include btn-blue-medium; + } + &.btn-info { @include btn-blue; } + &.btn-close, &.btn-warning { @include btn-orange; } @@ -110,20 +121,8 @@ float: right; } - &.btn-close { - color: $gl-danger; - border-color: $gl-danger; - &:hover { - color: #B94A48; - } - } - &.btn-reopen { - color: $gl-success; - border-color: $gl-success; - &:hover { - color: #468847; - } + /* should be same as parent class for now */ } &.btn-grouped { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 88da799ee2b247c93b70e377c6da33939f51dcd4..11730000f85e32f06b75fe0c282f7c3e0e00d802 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -374,7 +374,7 @@ table { } } -.center-top-menu { +.center-top-menu, .left-top-menu { @include nav-menu; text-align: center; margin-top: 5px; @@ -401,6 +401,16 @@ table { border-bottom: 1px solid $border-color; height: 57px; } + + &.wide { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + } +} + +.left-top-menu { + text-align: left; + border-bottom: 1px solid #EEE; } .center-middle-menu { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index f12d68b5a1fe32c8648f87a3d7201665cad526d3..e93dbab0c423673007ae683e417780c04031c27f 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -4,8 +4,8 @@ * */ -.issue-box { - @include border-radius(2px); +.status-box { + @include border-radius(3px); display: block; float: left; @@ -14,22 +14,22 @@ margin-right: 10px; font-size: $gl-font-size; - &.issue-box-closed { + &.status-box-closed { background-color: $gl-danger; color: #FFF; } - &.issue-box-merged { + &.status-box-merged { background-color: $gl-primary; color: #FFF; } - &.issue-box-open { - background-color: #019875; + &.status-box-open { + background-color: $green-light; color: #FFF; } - &.issue-box-expired { + &.status-box-expired { background: #cea61b; color: #FFF; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index aa5acb93cc52cb96882493e1300bfe3ba94128bc..a1a9990241d6964d96bda9f13e882c2c7ae9c66e 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -5,7 +5,7 @@ html { } body { - background-color: #EAEBEC !important; + background-color: #F3F3F3 !important; &.navless { background-color: white !important; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index cc48f8c8166324a874a0cbc3c545222e68dce21e..1c74e525a608348f04250da3177c9fb01ce15838 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -143,7 +143,11 @@ ul.controls { > li { float: left; - padding-right: 10px; + margin-right: 10px; + + &:last-child { + margin-right: 0; + } .author_link { display: inline-block; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 2b044786738566bae2ef7ab6a04693d11393ec95..4a00a197d9a0c817eaa19345e81fe6b176234e46 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -87,7 +87,7 @@ .new_note, .edit_note, -.issuable-description, +.detail-page-description, .milestone-description, .wiki-content, .merge-request-form { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 11c48d26ab53a27e50564b4c261ce0224653e6b0..41fd890f14f8046b858b3e6f56fffb4dd16ba338 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -123,7 +123,6 @@ padding: 0; margin: 0; list-style: none; - margin-top: 5px; height: 56px; li { @@ -131,9 +130,9 @@ a { padding: 14px; - font-size: 17px; + font-size: 15px; line-height: 28px; - color: #7f8fa4; + color: #959494; border-bottom: 2px solid transparent; &:hover, &:active, &:focus { @@ -143,8 +142,8 @@ } &.active a { - color: #4c4e54; - border-bottom: 2px solid #1cacfc; + color: #616060; + border-bottom: 2px solid #4688f1; } .badge { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 6f44c323732325d73eed22088c5ee1f8c43883b7..c00709fb6bb9a3bb1d7f07080ed77ff2502610a1 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -81,7 +81,7 @@ display: none; } - .center-top-menu { + .center-top-menu, .left-top-menu { li a { font-size: 14px; padding: 19px 10px; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index eb53c4153d3e794e48443da57f213ae6315538cd..ff41e26ed8adc4185f783f34a567820392d27e44 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -10,8 +10,7 @@ margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; - border-bottom: 1px solid #ECEEF1; - border-right: 1px solid #ECEEF1; + border-bottom: 1px solid $border-white-light; &:target { background: $hover; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2ef40a6e51738724399a45b592faf9a30c757152..af75123b0af367508648ead871ee196e231dc049 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,9 +1,9 @@ -$hover: #FFFAF1; +$hover: #faf9f9; $gl-text-color: #54565B; $gl-text-green: #4A2; $gl-text-red: #D12F19; $gl-text-orange: #D90; -$gl-header-color: #4c4e54; +$gl-header-color: #323232; $gl-link-color: #333c48; $md-text-color: #444; $md-link-color: #3084bb; @@ -15,13 +15,14 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #dce0e6; +$border-color: #efeff1; $table-border-color: #eef0f2; -$background-color: #F7F8FA; +$background-color: #faf9f9; $header-height: 58px; $fixed-layout-width: 1280px; -$gl-gray: #7f8fa4; +$gl-gray: #5a5a5a; $gl-padding: 16px; +$gl-padding-top:10px; $gl-avatar-size: 46px; /* @@ -29,12 +30,12 @@ $gl-avatar-size: 46px; */ $white-light: #FFFFFF; -$white-normal: #DCE0E5; -$white-dark: #E4E7ED; +$white-normal: #ededed; +$white-dark: #ededed; -$gray-light: #F0F2F5; -$gray-normal: #DCE0E5; -$gray-dark: #E4E7ED; +$gray-light: #f7f7f7; +$gray-normal: #ededed; +$gray-dark: #ededed; $green-light: #31AF64; $green-normal: #2FAA60; @@ -44,6 +45,10 @@ $blue-light: #2EA8E5; $blue-normal: #2D9FD8; $blue-dark: #2897CE; +$blue-medium-light: #3498CB; +$blue-medium: #2F8EBF; +$blue-medium-dark: #2D86B4; + $orange-light: #FC6443; $orange-normal: #E75E40; $orange-dark: #CE5237; @@ -52,11 +57,11 @@ $red-light: #F43263; $red-normal: #E52C5A; $red-dark: #D22852; -$border-white-light: #E3E7EC; +$border-white-light: #F1F2F4; $border-white-normal: #D6DAE2; $border-white-dark: #C6CACF; -$border-gray-light: #DCE0E5; +$border-gray-light: #d1d1d1; $border-gray-normal: #D6DAE2; $border-gray-dark: #C6CACF; @@ -76,6 +81,8 @@ $border-red-light: #E52C5A; $border-red-normal: #D22852; $border-red-dark: #CA264F; +/* header */ +$light-grey-header: #faf9f9; /* * State colors: diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 041b811a606b6f418e6417cb3e27fd4c43c04135..87dd30f41114daf3ab6d6bad96e781b38aadcaab 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -2,6 +2,12 @@ @include clearfix; line-height: 34px; + .emoji-icon { + width: 20px; + height: 20px; + margin: 7px 0 0 5px; + } + .award { @include border-radius(5px); @@ -40,6 +46,7 @@ } .awards-controls { + position: relative; margin-left: 10px; float: left; @@ -55,32 +62,64 @@ } } - .awards-menu { - padding: $gl-padding; - min-width: 214px; + .emoji-menu{ + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + + .emoji-menu-content { + padding: $gl-padding; + width: 300px; + height: 300px; + overflow-y: scroll; + + h5 { + clear: left; + } - > li { - cursor: pointer; - width: 30px; - height: 30px; - text-align: center; - @include border-radius(5px); + ul { + list-style-type: none; + margin-left: -20px; + margin-bottom: 20px; + overflow: auto; + } - img { - margin-bottom: 2px; + input.emoji-search{ + background: image-url("icon-search.png") 240px no-repeat; } - &:hover { - background-color: #ccc; + li { + cursor: pointer; + width: 30px; + height: 30px; + text-align: center; + float: left; + margin: 3px; + list-decorate: none; + @include border-radius(5px); + + &:hover { + background-color: #ccc; + } } } } } - - .awards-menu{ - li { - float: left; - margin: 3px; - } - } } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss new file mode 100644 index 0000000000000000000000000000000000000000..deab805dbc2988ab003f6f5b47675e7cb1697a92 --- /dev/null +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -0,0 +1,33 @@ +.detail-page-header { + margin: -$gl-padding; + padding: 7px $gl-padding; + margin-bottom: 0px; + border-bottom: 1px solid $border-color; + color: #5c5d5e; + font-size: 16px; + line-height: 34px; + + .author { + color: #5c5d5e; + } + + .identifier { + color: #5c5d5e; + } +} + +.detail-page-description { + .title { + margin: 0; + font-size: 23px; + color: #313236; + } + + .description { + margin-top: 6px; + + p:last-child { + margin-bottom: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss new file mode 100644 index 0000000000000000000000000000000000000000..89a94c5a780b5a7a552436475251d008370ada23 --- /dev/null +++ b/app/assets/stylesheets/pages/emojis.scss @@ -0,0 +1,1272 @@ +/* +File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy +The source: gemojione gem. +*/ + +.emoji-icon{ + background-image: image-url("emoji.png"); + background-repeat: no-repeat; +} + +.emoji-0023-20E3 { background-position: 0px 0px; } +.emoji-0030-20E3 { background-position: -20px 0px; } +.emoji-0031-20E3 { background-position: -40px 0px; } +.emoji-0032-20E3 { background-position: -60px 0px; } +.emoji-0033-20E3 { background-position: -80px 0px; } +.emoji-0034-20E3 { background-position: -100px 0px; } +.emoji-0035-20E3 { background-position: -120px 0px; } +.emoji-0036-20E3 { background-position: -140px 0px; } +.emoji-0037-20E3 { background-position: -160px 0px; } +.emoji-0038-20E3 { background-position: -180px 0px; } +.emoji-0039-20E3 { background-position: -200px 0px; } +.emoji-00A9 { background-position: -220px 0px; } +.emoji-00AE { background-position: -240px 0px; } +.emoji-1F004 { background-position: -260px 0px; } +.emoji-1F0CF { background-position: -280px 0px; } +.emoji-1F170 { background-position: -300px 0px; } +.emoji-1F171 { background-position: -320px 0px; } +.emoji-1F17E { background-position: -340px 0px; } +.emoji-1F17F { background-position: -360px 0px; } +.emoji-1F18E { background-position: -380px 0px; } +.emoji-1F191 { background-position: -400px 0px; } +.emoji-1F192 { background-position: -420px 0px; } +.emoji-1F193 { background-position: -440px 0px; } +.emoji-1F194 { background-position: -460px 0px; } +.emoji-1F195 { background-position: -480px 0px; } +.emoji-1F196 { background-position: -500px 0px; } +.emoji-1F197 { background-position: -520px 0px; } +.emoji-1F198 { background-position: -540px 0px; } +.emoji-1F199 { background-position: -560px 0px; } +.emoji-1F19A { background-position: -580px 0px; } +.emoji-1F1E6-1F1E8 { background-position: -600px 0px; } +.emoji-1F1E6-1F1E9 { background-position: -620px 0px; } +.emoji-1F1E6-1F1EA { background-position: -640px 0px; } +.emoji-1F1E6-1F1EB { background-position: -660px 0px; } +.emoji-1F1E6-1F1EC { background-position: -680px 0px; } +.emoji-1F1E6-1F1EE { background-position: -700px 0px; } +.emoji-1F1E6-1F1F1 { background-position: -720px 0px; } +.emoji-1F1E6-1F1F2 { background-position: -740px 0px; } +.emoji-1F1E6-1F1F4 { background-position: -760px 0px; } +.emoji-1F1E6-1F1F7 { background-position: -780px 0px; } +.emoji-1F1E6-1F1F9 { background-position: -800px 0px; } +.emoji-1F1E6-1F1FA { background-position: -820px 0px; } +.emoji-1F1E6-1F1FC { background-position: -840px 0px; } +.emoji-1F1E6-1F1FF { background-position: -860px 0px; } +.emoji-1F1E7-1F1E6 { background-position: -880px 0px; } +.emoji-1F1E7-1F1E7 { background-position: -900px 0px; } +.emoji-1F1E7-1F1E9 { background-position: -920px 0px; } +.emoji-1F1E7-1F1EA { background-position: -940px 0px; } +.emoji-1F1E7-1F1EB { background-position: -960px 0px; } +.emoji-1F1E7-1F1EC { background-position: -980px 0px; } +.emoji-1F1E7-1F1ED { background-position: -1000px 0px; } +.emoji-1F1E7-1F1EE { background-position: -1020px 0px; } +.emoji-1F1E7-1F1EF { background-position: -1040px 0px; } +.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; } +.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; } +.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; } +.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; } +.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; } +.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; } +.emoji-1F1E7-1F1FC { background-position: -1180px 0px; } +.emoji-1F1E7-1F1FE { background-position: -1200px 0px; } +.emoji-1F1E7-1F1FF { background-position: -1220px 0px; } +.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; } +.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; } +.emoji-1F1E8-1F1EB { background-position: -1280px 0px; } +.emoji-1F1E8-1F1EC { background-position: -1300px 0px; } +.emoji-1F1E8-1F1ED { background-position: -1320px 0px; } +.emoji-1F1E8-1F1EE { background-position: -1340px 0px; } +.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; } +.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; } +.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; } +.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; } +.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; } +.emoji-1F1E8-1F1FA { background-position: -1460px 0px; } +.emoji-1F1E8-1F1FB { background-position: -1480px 0px; } +.emoji-1F1E8-1F1FE { background-position: -1500px 0px; } +.emoji-1F1E8-1F1FF { background-position: -1520px 0px; } +.emoji-1F1E9-1F1EA { background-position: -1540px 0px; } +.emoji-1F1E9-1F1EF { background-position: -1560px 0px; } +.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; } +.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; } +.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; } +.emoji-1F1E9-1F1FF { background-position: -1640px 0px; } +.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; } +.emoji-1F1EA-1F1EA { background-position: -1680px 0px; } +.emoji-1F1EA-1F1EC { background-position: -1700px 0px; } +.emoji-1F1EA-1F1ED { background-position: -1720px 0px; } +.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; } +.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; } +.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; } +.emoji-1F1EB-1F1EE { background-position: -1800px 0px; } +.emoji-1F1EB-1F1EF { background-position: -1820px 0px; } +.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; } +.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; } +.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; } +.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; } +.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; } +.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; } +.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; } +.emoji-1F1EC-1F1EA { background-position: -1980px 0px; } +.emoji-1F1EC-1F1ED { background-position: -2000px 0px; } +.emoji-1F1EC-1F1EE { background-position: -2020px 0px; } +.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; } +.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; } +.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; } +.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; } +.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; } +.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; } +.emoji-1F1EC-1F1FA { background-position: -2160px 0px; } +.emoji-1F1EC-1F1FC { background-position: -2180px 0px; } +.emoji-1F1EC-1F1FE { background-position: -2200px 0px; } +.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; } +.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; } +.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; } +.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; } +.emoji-1F1ED-1F1FA { background-position: -2300px 0px; } +.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; } +.emoji-1F1EE-1F1EA { background-position: -2340px 0px; } +.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; } +.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; } +.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; } +.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; } +.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; } +.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; } +.emoji-1F1EF-1F1EA { background-position: -2480px 0px; } +.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; } +.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; } +.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; } +.emoji-1F1F0-1F1EA { background-position: -2560px 0px; } +.emoji-1F1F0-1F1EC { background-position: -2580px 0px; } +.emoji-1F1F0-1F1ED { background-position: -2600px 0px; } +.emoji-1F1F0-1F1EE { background-position: -2620px 0px; } +.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; } +.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; } +.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; } +.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; } +.emoji-1F1F0-1F1FC { background-position: -2720px 0px; } +.emoji-1F1F0-1F1FE { background-position: -2740px 0px; } +.emoji-1F1F0-1F1FF { background-position: -2760px 0px; } +.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; } +.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; } +.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; } +.emoji-1F1F1-1F1EE { background-position: -2840px 0px; } +.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; } +.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; } +.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; } +.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; } +.emoji-1F1F1-1F1FA { background-position: -2940px 0px; } +.emoji-1F1F1-1F1FB { background-position: -2960px 0px; } +.emoji-1F1F1-1F1FE { background-position: -2980px 0px; } +.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; } +.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; } +.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; } +.emoji-1F1F2-1F1EA { background-position: -3060px 0px; } +.emoji-1F1F2-1F1EC { background-position: -3080px 0px; } +.emoji-1F1F2-1F1ED { background-position: -3100px 0px; } +.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; } +.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; } +.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; } +.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; } +.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; } +.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; } +.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; } +.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; } +.emoji-1F1F2-1F1FA { background-position: -3280px 0px; } +.emoji-1F1F2-1F1FB { background-position: -3300px 0px; } +.emoji-1F1F2-1F1FC { background-position: -3320px 0px; } +.emoji-1F1F2-1F1FD { background-position: -3340px 0px; } +.emoji-1F1F2-1F1FE { background-position: -3360px 0px; } +.emoji-1F1F2-1F1FF { background-position: -3380px 0px; } +.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; } +.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; } +.emoji-1F1F3-1F1EA { background-position: -3440px 0px; } +.emoji-1F1F3-1F1EC { background-position: -3460px 0px; } +.emoji-1F1F3-1F1EE { background-position: -3480px 0px; } +.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; } +.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; } +.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; } +.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; } +.emoji-1F1F3-1F1FA { background-position: -3580px 0px; } +.emoji-1F1F3-1F1FF { background-position: -3600px 0px; } +.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; } +.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; } +.emoji-1F1F5-1F1EA { background-position: -3660px 0px; } +.emoji-1F1F5-1F1EB { background-position: -3680px 0px; } +.emoji-1F1F5-1F1EC { background-position: -3700px 0px; } +.emoji-1F1F5-1F1ED { background-position: -3720px 0px; } +.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; } +.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; } +.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; } +.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; } +.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; } +.emoji-1F1F5-1F1FC { background-position: -3840px 0px; } +.emoji-1F1F5-1F1FE { background-position: -3860px 0px; } +.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; } +.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; } +.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; } +.emoji-1F1F7-1F1FA { background-position: -3940px 0px; } +.emoji-1F1F7-1F1FC { background-position: -3960px 0px; } +.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; } +.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; } +.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; } +.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; } +.emoji-1F1F8-1F1EA { background-position: -4060px 0px; } +.emoji-1F1F8-1F1EC { background-position: -4080px 0px; } +.emoji-1F1F8-1F1ED { background-position: -4100px 0px; } +.emoji-1F1F8-1F1EE { background-position: -4120px 0px; } +.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; } +.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; } +.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; } +.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; } +.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; } +.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; } +.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; } +.emoji-1F1F8-1F1FB { background-position: -4280px 0px; } +.emoji-1F1F8-1F1FE { background-position: -4300px 0px; } +.emoji-1F1F8-1F1FF { background-position: -4320px 0px; } +.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; } +.emoji-1F1F9-1F1EC { background-position: -4360px 0px; } +.emoji-1F1F9-1F1ED { background-position: -4380px 0px; } +.emoji-1F1F9-1F1EF { background-position: -4400px 0px; } +.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; } +.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; } +.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; } +.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; } +.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; } +.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; } +.emoji-1F1F9-1F1FB { background-position: -4540px 0px; } +.emoji-1F1F9-1F1FC { background-position: -4560px 0px; } +.emoji-1F1F9-1F1FF { background-position: -4580px 0px; } +.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; } +.emoji-1F1FA-1F1EC { background-position: -4620px 0px; } +.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; } +.emoji-1F1FA-1F1FE { background-position: -4660px 0px; } +.emoji-1F1FA-1F1FF { background-position: -4680px 0px; } +.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; } +.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; } +.emoji-1F1FB-1F1EA { background-position: -4740px 0px; } +.emoji-1F1FB-1F1EE { background-position: -4760px 0px; } +.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; } +.emoji-1F1FB-1F1FA { background-position: -4800px 0px; } +.emoji-1F1FC-1F1EB { background-position: -4820px 0px; } +.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; } +.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; } +.emoji-1F1FE-1F1EA { background-position: -4880px 0px; } +.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; } +.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; } +.emoji-1F1FF-1F1FC { background-position: -4940px 0px; } +.emoji-1F201 { background-position: -4960px 0px; } +.emoji-1F202 { background-position: -4980px 0px; } +.emoji-1F21A { background-position: -5000px 0px; } +.emoji-1F22F { background-position: -5020px 0px; } +.emoji-1F232 { background-position: -5040px 0px; } +.emoji-1F233 { background-position: -5060px 0px; } +.emoji-1F234 { background-position: -5080px 0px; } +.emoji-1F235 { background-position: -5100px 0px; } +.emoji-1F236 { background-position: -5120px 0px; } +.emoji-1F237 { background-position: -5140px 0px; } +.emoji-1F238 { background-position: -5160px 0px; } +.emoji-1F239 { background-position: -5180px 0px; } +.emoji-1F23A { background-position: -5200px 0px; } +.emoji-1F250 { background-position: -5220px 0px; } +.emoji-1F251 { background-position: -5240px 0px; } +.emoji-1F300 { background-position: -5260px 0px; } +.emoji-1F301 { background-position: -5280px 0px; } +.emoji-1F302 { background-position: -5300px 0px; } +.emoji-1F303 { background-position: -5320px 0px; } +.emoji-1F304 { background-position: -5340px 0px; } +.emoji-1F305 { background-position: -5360px 0px; } +.emoji-1F306 { background-position: -5380px 0px; } +.emoji-1F307 { background-position: -5400px 0px; } +.emoji-1F308 { background-position: -5420px 0px; } +.emoji-1F309 { background-position: -5440px 0px; } +.emoji-1F30A { background-position: -5460px 0px; } +.emoji-1F30B { background-position: -5480px 0px; } +.emoji-1F30C { background-position: -5500px 0px; } +.emoji-1F30D { background-position: -5520px 0px; } +.emoji-1F30E { background-position: -5540px 0px; } +.emoji-1F30F { background-position: -5560px 0px; } +.emoji-1F310 { background-position: -5580px 0px; } +.emoji-1F311 { background-position: -5600px 0px; } +.emoji-1F312 { background-position: -5620px 0px; } +.emoji-1F313 { background-position: -5640px 0px; } +.emoji-1F314 { background-position: -5660px 0px; } +.emoji-1F315 { background-position: -5680px 0px; } +.emoji-1F316 { background-position: -5700px 0px; } +.emoji-1F317 { background-position: -5720px 0px; } +.emoji-1F318 { background-position: -5740px 0px; } +.emoji-1F319 { background-position: -5760px 0px; } +.emoji-1F31A { background-position: -5780px 0px; } +.emoji-1F31B { background-position: -5800px 0px; } +.emoji-1F31C { background-position: -5820px 0px; } +.emoji-1F31D { background-position: -5840px 0px; } +.emoji-1F31E { background-position: -5860px 0px; } +.emoji-1F31F { background-position: -5880px 0px; } +.emoji-1F320 { background-position: -5900px 0px; } +.emoji-1F321 { background-position: -5920px 0px; } +.emoji-1F327 { background-position: -5940px 0px; } +.emoji-1F328 { background-position: -5960px 0px; } +.emoji-1F329 { background-position: -5980px 0px; } +.emoji-1F32A { background-position: -6000px 0px; } +.emoji-1F32B { background-position: -6020px 0px; } +.emoji-1F32C { background-position: -6040px 0px; } +.emoji-1F330 { background-position: -6060px 0px; } +.emoji-1F331 { background-position: -6080px 0px; } +.emoji-1F332 { background-position: -6100px 0px; } +.emoji-1F333 { background-position: -6120px 0px; } +.emoji-1F334 { background-position: -6140px 0px; } +.emoji-1F335 { background-position: -6160px 0px; } +.emoji-1F336 { background-position: -6180px 0px; } +.emoji-1F337 { background-position: -6200px 0px; } +.emoji-1F338 { background-position: -6220px 0px; } +.emoji-1F339 { background-position: -6240px 0px; } +.emoji-1F33A { background-position: -6260px 0px; } +.emoji-1F33B { background-position: -6280px 0px; } +.emoji-1F33C { background-position: -6300px 0px; } +.emoji-1F33D { background-position: -6320px 0px; } +.emoji-1F33E { background-position: -6340px 0px; } +.emoji-1F33F { background-position: -6360px 0px; } +.emoji-1F340 { background-position: -6380px 0px; } +.emoji-1F341 { background-position: -6400px 0px; } +.emoji-1F342 { background-position: -6420px 0px; } +.emoji-1F343 { background-position: -6440px 0px; } +.emoji-1F344 { background-position: -6460px 0px; } +.emoji-1F345 { background-position: -6480px 0px; } +.emoji-1F346 { background-position: -6500px 0px; } +.emoji-1F347 { background-position: -6520px 0px; } +.emoji-1F348 { background-position: -6540px 0px; } +.emoji-1F349 { background-position: -6560px 0px; } +.emoji-1F34A { background-position: -6580px 0px; } +.emoji-1F34B { background-position: -6600px 0px; } +.emoji-1F34C { background-position: -6620px 0px; } +.emoji-1F34D { background-position: -6640px 0px; } +.emoji-1F34E { background-position: -6660px 0px; } +.emoji-1F34F { background-position: -6680px 0px; } +.emoji-1F350 { background-position: -6700px 0px; } +.emoji-1F351 { background-position: -6720px 0px; } +.emoji-1F352 { background-position: -6740px 0px; } +.emoji-1F353 { background-position: -6760px 0px; } +.emoji-1F354 { background-position: -6780px 0px; } +.emoji-1F355 { background-position: -6800px 0px; } +.emoji-1F356 { background-position: -6820px 0px; } +.emoji-1F357 { background-position: -6840px 0px; } +.emoji-1F358 { background-position: -6860px 0px; } +.emoji-1F359 { background-position: -6880px 0px; } +.emoji-1F35A { background-position: -6900px 0px; } +.emoji-1F35B { background-position: -6920px 0px; } +.emoji-1F35C { background-position: -6940px 0px; } +.emoji-1F35D { background-position: -6960px 0px; } +.emoji-1F35E { background-position: -6980px 0px; } +.emoji-1F35F { background-position: -7000px 0px; } +.emoji-1F360 { background-position: -7020px 0px; } +.emoji-1F361 { background-position: -7040px 0px; } +.emoji-1F362 { background-position: -7060px 0px; } +.emoji-1F363 { background-position: -7080px 0px; } +.emoji-1F364 { background-position: -7100px 0px; } +.emoji-1F365 { background-position: -7120px 0px; } +.emoji-1F366 { background-position: -7140px 0px; } +.emoji-1F367 { background-position: -7160px 0px; } +.emoji-1F368 { background-position: -7180px 0px; } +.emoji-1F369 { background-position: -7200px 0px; } +.emoji-1F36A { background-position: -7220px 0px; } +.emoji-1F36B { background-position: -7240px 0px; } +.emoji-1F36C { background-position: -7260px 0px; } +.emoji-1F36D { background-position: -7280px 0px; } +.emoji-1F36E { background-position: -7300px 0px; } +.emoji-1F36F { background-position: -7320px 0px; } +.emoji-1F370 { background-position: -7340px 0px; } +.emoji-1F371 { background-position: -7360px 0px; } +.emoji-1F372 { background-position: -7380px 0px; } +.emoji-1F373 { background-position: -7400px 0px; } +.emoji-1F374 { background-position: -7420px 0px; } +.emoji-1F375 { background-position: -7440px 0px; } +.emoji-1F376 { background-position: -7460px 0px; } +.emoji-1F377 { background-position: -7480px 0px; } +.emoji-1F378 { background-position: -7500px 0px; } +.emoji-1F379 { background-position: -7520px 0px; } +.emoji-1F37A { background-position: -7540px 0px; } +.emoji-1F37B { background-position: -7560px 0px; } +.emoji-1F37C { background-position: -7580px 0px; } +.emoji-1F37D { background-position: -7600px 0px; } +.emoji-1F380 { background-position: -7620px 0px; } +.emoji-1F381 { background-position: -7640px 0px; } +.emoji-1F382 { background-position: -7660px 0px; } +.emoji-1F383 { background-position: -7680px 0px; } +.emoji-1F384 { background-position: -7700px 0px; } +.emoji-1F385 { background-position: -7720px 0px; } +.emoji-1F386 { background-position: -7740px 0px; } +.emoji-1F387 { background-position: -7760px 0px; } +.emoji-1F388 { background-position: -7780px 0px; } +.emoji-1F389 { background-position: -7800px 0px; } +.emoji-1F38A { background-position: -7820px 0px; } +.emoji-1F38B { background-position: -7840px 0px; } +.emoji-1F38C { background-position: -7860px 0px; } +.emoji-1F38D { background-position: -7880px 0px; } +.emoji-1F38E { background-position: -7900px 0px; } +.emoji-1F38F { background-position: -7920px 0px; } +.emoji-1F390 { background-position: -7940px 0px; } +.emoji-1F391 { background-position: -7960px 0px; } +.emoji-1F392 { background-position: -7980px 0px; } +.emoji-1F393 { background-position: -8000px 0px; } +.emoji-1F394 { background-position: -8020px 0px; } +.emoji-1F395 { background-position: -8040px 0px; } +.emoji-1F396 { background-position: -8060px 0px; } +.emoji-1F397 { background-position: -8080px 0px; } +.emoji-1F398 { background-position: -8100px 0px; } +.emoji-1F399 { background-position: -8120px 0px; } +.emoji-1F39A { background-position: -8140px 0px; } +.emoji-1F39B { background-position: -8160px 0px; } +.emoji-1F39C { background-position: -8180px 0px; } +.emoji-1F39D { background-position: -8200px 0px; } +.emoji-1F39E { background-position: -8220px 0px; } +.emoji-1F39F { background-position: -8240px 0px; } +.emoji-1F3A0 { background-position: -8260px 0px; } +.emoji-1F3A1 { background-position: -8280px 0px; } +.emoji-1F3A2 { background-position: -8300px 0px; } +.emoji-1F3A3 { background-position: -8320px 0px; } +.emoji-1F3A4 { background-position: -8340px 0px; } +.emoji-1F3A5 { background-position: -8360px 0px; } +.emoji-1F3A6 { background-position: -8380px 0px; } +.emoji-1F3A7 { background-position: -8400px 0px; } +.emoji-1F3A8 { background-position: -8420px 0px; } +.emoji-1F3A9 { background-position: -8440px 0px; } +.emoji-1F3AA { background-position: -8460px 0px; } +.emoji-1F3AB { background-position: -8480px 0px; } +.emoji-1F3AC { background-position: -8500px 0px; } +.emoji-1F3AD { background-position: -8520px 0px; } +.emoji-1F3AE { background-position: -8540px 0px; } +.emoji-1F3AF { background-position: -8560px 0px; } +.emoji-1F3B0 { background-position: -8580px 0px; } +.emoji-1F3B1 { background-position: -8600px 0px; } +.emoji-1F3B2 { background-position: -8620px 0px; } +.emoji-1F3B3 { background-position: -8640px 0px; } +.emoji-1F3B4 { background-position: -8660px 0px; } +.emoji-1F3B5 { background-position: -8680px 0px; } +.emoji-1F3B6 { background-position: -8700px 0px; } +.emoji-1F3B7 { background-position: -8720px 0px; } +.emoji-1F3B8 { background-position: -8740px 0px; } +.emoji-1F3B9 { background-position: -8760px 0px; } +.emoji-1F3BA { background-position: -8780px 0px; } +.emoji-1F3BB { background-position: -8800px 0px; } +.emoji-1F3BC { background-position: -8820px 0px; } +.emoji-1F3BD { background-position: -8840px 0px; } +.emoji-1F3BE { background-position: -8860px 0px; } +.emoji-1F3BF { background-position: -8880px 0px; } +.emoji-1F3C0 { background-position: -8900px 0px; } +.emoji-1F3C1 { background-position: -8920px 0px; } +.emoji-1F3C2 { background-position: -8940px 0px; } +.emoji-1F3C3 { background-position: -8960px 0px; } +.emoji-1F3C4 { background-position: -8980px 0px; } +.emoji-1F3C5 { background-position: -9000px 0px; } +.emoji-1F3C6 { background-position: -9020px 0px; } +.emoji-1F3C7 { background-position: -9040px 0px; } +.emoji-1F3C8 { background-position: -9060px 0px; } +.emoji-1F3C9 { background-position: -9080px 0px; } +.emoji-1F3CA { background-position: -9100px 0px; } +.emoji-1F3CB { background-position: -9120px 0px; } +.emoji-1F3CC { background-position: -9140px 0px; } +.emoji-1F3CD { background-position: -9160px 0px; } +.emoji-1F3CE { background-position: -9180px 0px; } +.emoji-1F3D4 { background-position: -9200px 0px; } +.emoji-1F3D5 { background-position: -9220px 0px; } +.emoji-1F3D6 { background-position: -9240px 0px; } +.emoji-1F3D7 { background-position: -9260px 0px; } +.emoji-1F3D8 { background-position: -9280px 0px; } +.emoji-1F3D9 { background-position: -9300px 0px; } +.emoji-1F3DA { background-position: -9320px 0px; } +.emoji-1F3DB { background-position: -9340px 0px; } +.emoji-1F3DC { background-position: -9360px 0px; } +.emoji-1F3DD { background-position: -9380px 0px; } +.emoji-1F3DE { background-position: -9400px 0px; } +.emoji-1F3DF { background-position: -9420px 0px; } +.emoji-1F3E0 { background-position: -9440px 0px; } +.emoji-1F3E1 { background-position: -9460px 0px; } +.emoji-1F3E2 { background-position: -9480px 0px; } +.emoji-1F3E3 { background-position: -9500px 0px; } +.emoji-1F3E4 { background-position: -9520px 0px; } +.emoji-1F3E5 { background-position: -9540px 0px; } +.emoji-1F3E6 { background-position: -9560px 0px; } +.emoji-1F3E7 { background-position: -9580px 0px; } +.emoji-1F3E8 { background-position: -9600px 0px; } +.emoji-1F3E9 { background-position: -9620px 0px; } +.emoji-1F3EA { background-position: -9640px 0px; } +.emoji-1F3EB { background-position: -9660px 0px; } +.emoji-1F3EC { background-position: -9680px 0px; } +.emoji-1F3ED { background-position: -9700px 0px; } +.emoji-1F3EE { background-position: -9720px 0px; } +.emoji-1F3EF { background-position: -9740px 0px; } +.emoji-1F3F0 { background-position: -9760px 0px; } +.emoji-1F3F1 { background-position: -9780px 0px; } +.emoji-1F3F2 { background-position: -9800px 0px; } +.emoji-1F3F3 { background-position: -9820px 0px; } +.emoji-1F3F4 { background-position: -9840px 0px; } +.emoji-1F3F5 { background-position: -9860px 0px; } +.emoji-1F3F6 { background-position: -9880px 0px; } +.emoji-1F3F7 { background-position: -9900px 0px; } +.emoji-1F400 { background-position: -9920px 0px; } +.emoji-1F401 { background-position: -9940px 0px; } +.emoji-1F402 { background-position: -9960px 0px; } +.emoji-1F403 { background-position: -9980px 0px; } +.emoji-1F404 { background-position: -10000px 0px; } +.emoji-1F405 { background-position: -10020px 0px; } +.emoji-1F406 { background-position: -10040px 0px; } +.emoji-1F407 { background-position: -10060px 0px; } +.emoji-1F408 { background-position: -10080px 0px; } +.emoji-1F409 { background-position: -10100px 0px; } +.emoji-1F40A { background-position: -10120px 0px; } +.emoji-1F40B { background-position: -10140px 0px; } +.emoji-1F40C { background-position: -10160px 0px; } +.emoji-1F40D { background-position: -10180px 0px; } +.emoji-1F40E { background-position: -10200px 0px; } +.emoji-1F40F { background-position: -10220px 0px; } +.emoji-1F410 { background-position: -10240px 0px; } +.emoji-1F411 { background-position: -10260px 0px; } +.emoji-1F412 { background-position: -10280px 0px; } +.emoji-1F413 { background-position: -10300px 0px; } +.emoji-1F414 { background-position: -10320px 0px; } +.emoji-1F415 { background-position: -10340px 0px; } +.emoji-1F416 { background-position: -10360px 0px; } +.emoji-1F417 { background-position: -10380px 0px; } +.emoji-1F418 { background-position: -10400px 0px; } +.emoji-1F419 { background-position: -10420px 0px; } +.emoji-1F41A { background-position: -10440px 0px; } +.emoji-1F41B { background-position: -10460px 0px; } +.emoji-1F41C { background-position: -10480px 0px; } +.emoji-1F41D { background-position: -10500px 0px; } +.emoji-1F41E { background-position: -10520px 0px; } +.emoji-1F41F { background-position: -10540px 0px; } +.emoji-1F420 { background-position: -10560px 0px; } +.emoji-1F421 { background-position: -10580px 0px; } +.emoji-1F422 { background-position: -10600px 0px; } +.emoji-1F423 { background-position: -10620px 0px; } +.emoji-1F424 { background-position: -10640px 0px; } +.emoji-1F425 { background-position: -10660px 0px; } +.emoji-1F426 { background-position: -10680px 0px; } +.emoji-1F427 { background-position: -10700px 0px; } +.emoji-1F428 { background-position: -10720px 0px; } +.emoji-1F429 { background-position: -10740px 0px; } +.emoji-1F42A { background-position: -10760px 0px; } +.emoji-1F42B { background-position: -10780px 0px; } +.emoji-1F42C { background-position: -10800px 0px; } +.emoji-1F42D { background-position: -10820px 0px; } +.emoji-1F42E { background-position: -10840px 0px; } +.emoji-1F42F { background-position: -10860px 0px; } +.emoji-1F430 { background-position: -10880px 0px; } +.emoji-1F431 { background-position: -10900px 0px; } +.emoji-1F432 { background-position: -10920px 0px; } +.emoji-1F433 { background-position: -10940px 0px; } +.emoji-1F434 { background-position: -10960px 0px; } +.emoji-1F435 { background-position: -10980px 0px; } +.emoji-1F436 { background-position: -11000px 0px; } +.emoji-1F437 { background-position: -11020px 0px; } +.emoji-1F438 { background-position: -11040px 0px; } +.emoji-1F439 { background-position: -11060px 0px; } +.emoji-1F43A { background-position: -11080px 0px; } +.emoji-1F43B { background-position: -11100px 0px; } +.emoji-1F43C { background-position: -11120px 0px; } +.emoji-1F43D { background-position: -11140px 0px; } +.emoji-1F43E { background-position: -11160px 0px; } +.emoji-1F43F { background-position: -11180px 0px; } +.emoji-1F440 { background-position: -11200px 0px; } +.emoji-1F441 { background-position: -11220px 0px; } +.emoji-1F442 { background-position: -11240px 0px; } +.emoji-1F443 { background-position: -11260px 0px; } +.emoji-1F444 { background-position: -11280px 0px; } +.emoji-1F445 { background-position: -11300px 0px; } +.emoji-1F446 { background-position: -11320px 0px; } +.emoji-1F447 { background-position: -11340px 0px; } +.emoji-1F448 { background-position: -11360px 0px; } +.emoji-1F449 { background-position: -11380px 0px; } +.emoji-1F44A { background-position: -11400px 0px; } +.emoji-1F44B { background-position: -11420px 0px; } +.emoji-1F44C { background-position: -11440px 0px; } +.emoji-1F44D { background-position: -11460px 0px; } +.emoji-1F44E { background-position: -11480px 0px; } +.emoji-1F44F { background-position: -11500px 0px; } +.emoji-1F450 { background-position: -11520px 0px; } +.emoji-1F451 { background-position: -11540px 0px; } +.emoji-1F452 { background-position: -11560px 0px; } +.emoji-1F453 { background-position: -11580px 0px; } +.emoji-1F454 { background-position: -11600px 0px; } +.emoji-1F455 { background-position: -11620px 0px; } +.emoji-1F456 { background-position: -11640px 0px; } +.emoji-1F457 { background-position: -11660px 0px; } +.emoji-1F458 { background-position: -11680px 0px; } +.emoji-1F459 { background-position: -11700px 0px; } +.emoji-1F45A { background-position: -11720px 0px; } +.emoji-1F45B { background-position: -11740px 0px; } +.emoji-1F45C { background-position: -11760px 0px; } +.emoji-1F45D { background-position: -11780px 0px; } +.emoji-1F45E { background-position: -11800px 0px; } +.emoji-1F45F { background-position: -11820px 0px; } +.emoji-1F460 { background-position: -11840px 0px; } +.emoji-1F461 { background-position: -11860px 0px; } +.emoji-1F462 { background-position: -11880px 0px; } +.emoji-1F463 { background-position: -11900px 0px; } +.emoji-1F464 { background-position: -11920px 0px; } +.emoji-1F465 { background-position: -11940px 0px; } +.emoji-1F466 { background-position: -11960px 0px; } +.emoji-1F467 { background-position: -11980px 0px; } +.emoji-1F468 { background-position: -12000px 0px; } +.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; } +.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; } +.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; } +.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; } +.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; } +.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; } +.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; } +.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; } +.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; } +.emoji-1F468-2764-1F468 { background-position: -12200px 0px; } +.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; } +.emoji-1F469 { background-position: -12240px 0px; } +.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; } +.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; } +.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; } +.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; } +.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; } +.emoji-1F469-2764-1F469 { background-position: -12360px 0px; } +.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; } +.emoji-1F46A { background-position: -12400px 0px; } +.emoji-1F46B { background-position: -12420px 0px; } +.emoji-1F46C { background-position: -12440px 0px; } +.emoji-1F46D { background-position: -12460px 0px; } +.emoji-1F46E { background-position: -12480px 0px; } +.emoji-1F46F { background-position: -12500px 0px; } +.emoji-1F470 { background-position: -12520px 0px; } +.emoji-1F471 { background-position: -12540px 0px; } +.emoji-1F472 { background-position: -12560px 0px; } +.emoji-1F473 { background-position: -12580px 0px; } +.emoji-1F474 { background-position: -12600px 0px; } +.emoji-1F475 { background-position: -12620px 0px; } +.emoji-1F476 { background-position: -12640px 0px; } +.emoji-1F477 { background-position: -12660px 0px; } +.emoji-1F478 { background-position: -12680px 0px; } +.emoji-1F479 { background-position: -12700px 0px; } +.emoji-1F47A { background-position: -12720px 0px; } +.emoji-1F47B { background-position: -12740px 0px; } +.emoji-1F47C { background-position: -12760px 0px; } +.emoji-1F47D { background-position: -12780px 0px; } +.emoji-1F47E { background-position: -12800px 0px; } +.emoji-1F47F { background-position: -12820px 0px; } +.emoji-1F480 { background-position: -12840px 0px; } +.emoji-1F481 { background-position: -12860px 0px; } +.emoji-1F482 { background-position: -12880px 0px; } +.emoji-1F483 { background-position: -12900px 0px; } +.emoji-1F484 { background-position: -12920px 0px; } +.emoji-1F485 { background-position: -12940px 0px; } +.emoji-1F486 { background-position: -12960px 0px; } +.emoji-1F487 { background-position: -12980px 0px; } +.emoji-1F488 { background-position: -13000px 0px; } +.emoji-1F489 { background-position: -13020px 0px; } +.emoji-1F48A { background-position: -13040px 0px; } +.emoji-1F48B { background-position: -13060px 0px; } +.emoji-1F48C { background-position: -13080px 0px; } +.emoji-1F48D { background-position: -13100px 0px; } +.emoji-1F48E { background-position: -13120px 0px; } +.emoji-1F48F { background-position: -13140px 0px; } +.emoji-1F490 { background-position: -13160px 0px; } +.emoji-1F491 { background-position: -13180px 0px; } +.emoji-1F492 { background-position: -13200px 0px; } +.emoji-1F493 { background-position: -13220px 0px; } +.emoji-1F494 { background-position: -13240px 0px; } +.emoji-1F495 { background-position: -13260px 0px; } +.emoji-1F496 { background-position: -13280px 0px; } +.emoji-1F497 { background-position: -13300px 0px; } +.emoji-1F498 { background-position: -13320px 0px; } +.emoji-1F499 { background-position: -13340px 0px; } +.emoji-1F49A { background-position: -13360px 0px; } +.emoji-1F49B { background-position: -13380px 0px; } +.emoji-1F49C { background-position: -13400px 0px; } +.emoji-1F49D { background-position: -13420px 0px; } +.emoji-1F49E { background-position: -13440px 0px; } +.emoji-1F49F { background-position: -13460px 0px; } +.emoji-1F4A0 { background-position: -13480px 0px; } +.emoji-1F4A1 { background-position: -13500px 0px; } +.emoji-1F4A2 { background-position: -13520px 0px; } +.emoji-1F4A3 { background-position: -13540px 0px; } +.emoji-1F4A4 { background-position: -13560px 0px; } +.emoji-1F4A5 { background-position: -13580px 0px; } +.emoji-1F4A6 { background-position: -13600px 0px; } +.emoji-1F4A7 { background-position: -13620px 0px; } +.emoji-1F4A8 { background-position: -13640px 0px; } +.emoji-1F4A9 { background-position: -13660px 0px; } +.emoji-1F4AA { background-position: -13680px 0px; } +.emoji-1F4AB { background-position: -13700px 0px; } +.emoji-1F4AC { background-position: -13720px 0px; } +.emoji-1F4AD { background-position: -13740px 0px; } +.emoji-1F4AE { background-position: -13760px 0px; } +.emoji-1F4AF { background-position: -13780px 0px; } +.emoji-1F4B0 { background-position: -13800px 0px; } +.emoji-1F4B1 { background-position: -13820px 0px; } +.emoji-1F4B2 { background-position: -13840px 0px; } +.emoji-1F4B3 { background-position: -13860px 0px; } +.emoji-1F4B4 { background-position: -13880px 0px; } +.emoji-1F4B5 { background-position: -13900px 0px; } +.emoji-1F4B6 { background-position: -13920px 0px; } +.emoji-1F4B7 { background-position: -13940px 0px; } +.emoji-1F4B8 { background-position: -13960px 0px; } +.emoji-1F4B9 { background-position: -13980px 0px; } +.emoji-1F4BA { background-position: -14000px 0px; } +.emoji-1F4BB { background-position: -14020px 0px; } +.emoji-1F4BC { background-position: -14040px 0px; } +.emoji-1F4BD { background-position: -14060px 0px; } +.emoji-1F4BE { background-position: -14080px 0px; } +.emoji-1F4BF { background-position: -14100px 0px; } +.emoji-1F4C0 { background-position: -14120px 0px; } +.emoji-1F4C1 { background-position: -14140px 0px; } +.emoji-1F4C2 { background-position: -14160px 0px; } +.emoji-1F4C3 { background-position: -14180px 0px; } +.emoji-1F4C4 { background-position: -14200px 0px; } +.emoji-1F4C5 { background-position: -14220px 0px; } +.emoji-1F4C6 { background-position: -14240px 0px; } +.emoji-1F4C7 { background-position: -14260px 0px; } +.emoji-1F4C8 { background-position: -14280px 0px; } +.emoji-1F4C9 { background-position: -14300px 0px; } +.emoji-1F4CA { background-position: -14320px 0px; } +.emoji-1F4CB { background-position: -14340px 0px; } +.emoji-1F4CC { background-position: -14360px 0px; } +.emoji-1F4CD { background-position: -14380px 0px; } +.emoji-1F4CE { background-position: -14400px 0px; } +.emoji-1F4CF { background-position: -14420px 0px; } +.emoji-1F4D0 { background-position: -14440px 0px; } +.emoji-1F4D1 { background-position: -14460px 0px; } +.emoji-1F4D2 { background-position: -14480px 0px; } +.emoji-1F4D3 { background-position: -14500px 0px; } +.emoji-1F4D4 { background-position: -14520px 0px; } +.emoji-1F4D5 { background-position: -14540px 0px; } +.emoji-1F4D6 { background-position: -14560px 0px; } +.emoji-1F4D7 { background-position: -14580px 0px; } +.emoji-1F4D8 { background-position: -14600px 0px; } +.emoji-1F4D9 { background-position: -14620px 0px; } +.emoji-1F4DA { background-position: -14640px 0px; } +.emoji-1F4DB { background-position: -14660px 0px; } +.emoji-1F4DC { background-position: -14680px 0px; } +.emoji-1F4DD { background-position: -14700px 0px; } +.emoji-1F4DE { background-position: -14720px 0px; } +.emoji-1F4DF { background-position: -14740px 0px; } +.emoji-1F4E0 { background-position: -14760px 0px; } +.emoji-1F4E1 { background-position: -14780px 0px; } +.emoji-1F4E2 { background-position: -14800px 0px; } +.emoji-1F4E3 { background-position: -14820px 0px; } +.emoji-1F4E4 { background-position: -14840px 0px; } +.emoji-1F4E5 { background-position: -14860px 0px; } +.emoji-1F4E6 { background-position: -14880px 0px; } +.emoji-1F4E7 { background-position: -14900px 0px; } +.emoji-1F4E8 { background-position: -14920px 0px; } +.emoji-1F4E9 { background-position: -14940px 0px; } +.emoji-1F4EA { background-position: -14960px 0px; } +.emoji-1F4EB { background-position: -14980px 0px; } +.emoji-1F4EC { background-position: -15000px 0px; } +.emoji-1F4ED { background-position: -15020px 0px; } +.emoji-1F4EE { background-position: -15040px 0px; } +.emoji-1F4EF { background-position: -15060px 0px; } +.emoji-1F4F0 { background-position: -15080px 0px; } +.emoji-1F4F1 { background-position: -15100px 0px; } +.emoji-1F4F2 { background-position: -15120px 0px; } +.emoji-1F4F3 { background-position: -15140px 0px; } +.emoji-1F4F4 { background-position: -15160px 0px; } +.emoji-1F4F5 { background-position: -15180px 0px; } +.emoji-1F4F6 { background-position: -15200px 0px; } +.emoji-1F4F7 { background-position: -15220px 0px; } +.emoji-1F4F8 { background-position: -15240px 0px; } +.emoji-1F4F9 { background-position: -15260px 0px; } +.emoji-1F4FA { background-position: -15280px 0px; } +.emoji-1F4FB { background-position: -15300px 0px; } +.emoji-1F4FC { background-position: -15320px 0px; } +.emoji-1F4FD { background-position: -15340px 0px; } +.emoji-1F4FE { background-position: -15360px 0px; } +.emoji-1F500 { background-position: -15380px 0px; } +.emoji-1F501 { background-position: -15400px 0px; } +.emoji-1F502 { background-position: -15420px 0px; } +.emoji-1F503 { background-position: -15440px 0px; } +.emoji-1F504 { background-position: -15460px 0px; } +.emoji-1F505 { background-position: -15480px 0px; } +.emoji-1F506 { background-position: -15500px 0px; } +.emoji-1F507 { background-position: -15520px 0px; } +.emoji-1F508 { background-position: -15540px 0px; } +.emoji-1F509 { background-position: -15560px 0px; } +.emoji-1F50A { background-position: -15580px 0px; } +.emoji-1F50B { background-position: -15600px 0px; } +.emoji-1F50C { background-position: -15620px 0px; } +.emoji-1F50D { background-position: -15640px 0px; } +.emoji-1F50E { background-position: -15660px 0px; } +.emoji-1F50F { background-position: -15680px 0px; } +.emoji-1F510 { background-position: -15700px 0px; } +.emoji-1F511 { background-position: -15720px 0px; } +.emoji-1F512 { background-position: -15740px 0px; } +.emoji-1F513 { background-position: -15760px 0px; } +.emoji-1F514 { background-position: -15780px 0px; } +.emoji-1F515 { background-position: -15800px 0px; } +.emoji-1F516 { background-position: -15820px 0px; } +.emoji-1F517 { background-position: -15840px 0px; } +.emoji-1F518 { background-position: -15860px 0px; } +.emoji-1F519 { background-position: -15880px 0px; } +.emoji-1F51A { background-position: -15900px 0px; } +.emoji-1F51B { background-position: -15920px 0px; } +.emoji-1F51C { background-position: -15940px 0px; } +.emoji-1F51D { background-position: -15960px 0px; } +.emoji-1F51E { background-position: -15980px 0px; } +.emoji-1F51F { background-position: -16000px 0px; } +.emoji-1F520 { background-position: -16020px 0px; } +.emoji-1F521 { background-position: -16040px 0px; } +.emoji-1F522 { background-position: -16060px 0px; } +.emoji-1F523 { background-position: -16080px 0px; } +.emoji-1F524 { background-position: -16100px 0px; } +.emoji-1F525 { background-position: -16120px 0px; } +.emoji-1F526 { background-position: -16140px 0px; } +.emoji-1F527 { background-position: -16160px 0px; } +.emoji-1F528 { background-position: -16180px 0px; } +.emoji-1F529 { background-position: -16200px 0px; } +.emoji-1F52A { background-position: -16220px 0px; } +.emoji-1F52B { background-position: -16240px 0px; } +.emoji-1F52C { background-position: -16260px 0px; } +.emoji-1F52D { background-position: -16280px 0px; } +.emoji-1F52E { background-position: -16300px 0px; } +.emoji-1F52F { background-position: -16320px 0px; } +.emoji-1F530 { background-position: -16340px 0px; } +.emoji-1F531 { background-position: -16360px 0px; } +.emoji-1F532 { background-position: -16380px 0px; } +.emoji-1F533 { background-position: -16400px 0px; } +.emoji-1F534 { background-position: -16420px 0px; } +.emoji-1F535 { background-position: -16440px 0px; } +.emoji-1F536 { background-position: -16460px 0px; } +.emoji-1F537 { background-position: -16480px 0px; } +.emoji-1F538 { background-position: -16500px 0px; } +.emoji-1F539 { background-position: -16520px 0px; } +.emoji-1F53A { background-position: -16540px 0px; } +.emoji-1F53B { background-position: -16560px 0px; } +.emoji-1F53C { background-position: -16580px 0px; } +.emoji-1F53D { background-position: -16600px 0px; } +.emoji-1F546 { background-position: -16620px 0px; } +.emoji-1F547 { background-position: -16640px 0px; } +.emoji-1F548 { background-position: -16660px 0px; } +.emoji-1F549 { background-position: -16680px 0px; } +.emoji-1F54A { background-position: -16700px 0px; } +.emoji-1F550 { background-position: -16720px 0px; } +.emoji-1F551 { background-position: -16740px 0px; } +.emoji-1F552 { background-position: -16760px 0px; } +.emoji-1F553 { background-position: -16780px 0px; } +.emoji-1F554 { background-position: -16800px 0px; } +.emoji-1F555 { background-position: -16820px 0px; } +.emoji-1F556 { background-position: -16840px 0px; } +.emoji-1F557 { background-position: -16860px 0px; } +.emoji-1F558 { background-position: -16880px 0px; } +.emoji-1F559 { background-position: -16900px 0px; } +.emoji-1F55A { background-position: -16920px 0px; } +.emoji-1F55B { background-position: -16940px 0px; } +.emoji-1F55C { background-position: -16960px 0px; } +.emoji-1F55D { background-position: -16980px 0px; } +.emoji-1F55E { background-position: -17000px 0px; } +.emoji-1F55F { background-position: -17020px 0px; } +.emoji-1F560 { background-position: -17040px 0px; } +.emoji-1F561 { background-position: -17060px 0px; } +.emoji-1F562 { background-position: -17080px 0px; } +.emoji-1F563 { background-position: -17100px 0px; } +.emoji-1F564 { background-position: -17120px 0px; } +.emoji-1F565 { background-position: -17140px 0px; } +.emoji-1F566 { background-position: -17160px 0px; } +.emoji-1F567 { background-position: -17180px 0px; } +.emoji-1F568 { background-position: -17200px 0px; } +.emoji-1F569 { background-position: -17220px 0px; } +.emoji-1F56A { background-position: -17240px 0px; } +.emoji-1F56B { background-position: -17260px 0px; } +.emoji-1F56C { background-position: -17280px 0px; } +.emoji-1F56D { background-position: -17300px 0px; } +.emoji-1F56E { background-position: -17320px 0px; } +.emoji-1F56F { background-position: -17340px 0px; } +.emoji-1F570 { background-position: -17360px 0px; } +.emoji-1F571 { background-position: -17380px 0px; } +.emoji-1F572 { background-position: -17400px 0px; } +.emoji-1F573 { background-position: -17420px 0px; } +.emoji-1F574 { background-position: -17440px 0px; } +.emoji-1F575 { background-position: -17460px 0px; } +.emoji-1F576 { background-position: -17480px 0px; } +.emoji-1F577 { background-position: -17500px 0px; } +.emoji-1F578 { background-position: -17520px 0px; } +.emoji-1F579 { background-position: -17540px 0px; } +.emoji-1F57B { background-position: -17560px 0px; } +.emoji-1F57E { background-position: -17580px 0px; } +.emoji-1F57F { background-position: -17600px 0px; } +.emoji-1F581 { background-position: -17620px 0px; } +.emoji-1F582 { background-position: -17640px 0px; } +.emoji-1F583 { background-position: -17660px 0px; } +.emoji-1F585 { background-position: -17680px 0px; } +.emoji-1F586 { background-position: -17700px 0px; } +.emoji-1F587 { background-position: -17720px 0px; } +.emoji-1F588 { background-position: -17740px 0px; } +.emoji-1F589 { background-position: -17760px 0px; } +.emoji-1F58A { background-position: -17780px 0px; } +.emoji-1F58B { background-position: -17800px 0px; } +.emoji-1F58C { background-position: -17820px 0px; } +.emoji-1F58D { background-position: -17840px 0px; } +.emoji-1F58E { background-position: -17860px 0px; } +.emoji-1F58F { background-position: -17880px 0px; } +.emoji-1F590 { background-position: -17900px 0px; } +.emoji-1F591 { background-position: -17920px 0px; } +.emoji-1F592 { background-position: -17940px 0px; } +.emoji-1F593 { background-position: -17960px 0px; } +.emoji-1F594 { background-position: -17980px 0px; } +.emoji-1F595 { background-position: -18000px 0px; } +.emoji-1F596 { background-position: -18020px 0px; } +.emoji-1F597 { background-position: -18040px 0px; } +.emoji-1F598 { background-position: -18060px 0px; } +.emoji-1F599 { background-position: -18080px 0px; } +.emoji-1F59E { background-position: -18100px 0px; } +.emoji-1F59F { background-position: -18120px 0px; } +.emoji-1F5A5 { background-position: -18140px 0px; } +.emoji-1F5A6 { background-position: -18160px 0px; } +.emoji-1F5A7 { background-position: -18180px 0px; } +.emoji-1F5A8 { background-position: -18200px 0px; } +.emoji-1F5A9 { background-position: -18220px 0px; } +.emoji-1F5AA { background-position: -18240px 0px; } +.emoji-1F5AB { background-position: -18260px 0px; } +.emoji-1F5AD { background-position: -18280px 0px; } +.emoji-1F5AE { background-position: -18300px 0px; } +.emoji-1F5AF { background-position: -18320px 0px; } +.emoji-1F5B2 { background-position: -18340px 0px; } +.emoji-1F5B3 { background-position: -18360px 0px; } +.emoji-1F5B4 { background-position: -18380px 0px; } +.emoji-1F5B8 { background-position: -18400px 0px; } +.emoji-1F5B9 { background-position: -18420px 0px; } +.emoji-1F5BC { background-position: -18440px 0px; } +.emoji-1F5BD { background-position: -18460px 0px; } +.emoji-1F5BE { background-position: -18480px 0px; } +.emoji-1F5C0 { background-position: -18500px 0px; } +.emoji-1F5C1 { background-position: -18520px 0px; } +.emoji-1F5C2 { background-position: -18540px 0px; } +.emoji-1F5C3 { background-position: -18560px 0px; } +.emoji-1F5C4 { background-position: -18580px 0px; } +.emoji-1F5C6 { background-position: -18600px 0px; } +.emoji-1F5C7 { background-position: -18620px 0px; } +.emoji-1F5C9 { background-position: -18640px 0px; } +.emoji-1F5CA { background-position: -18660px 0px; } +.emoji-1F5CE { background-position: -18680px 0px; } +.emoji-1F5CF { background-position: -18700px 0px; } +.emoji-1F5D0 { background-position: -18720px 0px; } +.emoji-1F5D1 { background-position: -18740px 0px; } +.emoji-1F5D2 { background-position: -18760px 0px; } +.emoji-1F5D3 { background-position: -18780px 0px; } +.emoji-1F5D4 { background-position: -18800px 0px; } +.emoji-1F5D8 { background-position: -18820px 0px; } +.emoji-1F5D9 { background-position: -18840px 0px; } +.emoji-1F5DC { background-position: -18860px 0px; } +.emoji-1F5DD { background-position: -18880px 0px; } +.emoji-1F5DE { background-position: -18900px 0px; } +.emoji-1F5E0 { background-position: -18920px 0px; } +.emoji-1F5E1 { background-position: -18940px 0px; } +.emoji-1F5E2 { background-position: -18960px 0px; } +.emoji-1F5E3 { background-position: -18980px 0px; } +.emoji-1F5E8 { background-position: -19000px 0px; } +.emoji-1F5E9 { background-position: -19020px 0px; } +.emoji-1F5EA { background-position: -19040px 0px; } +.emoji-1F5EB { background-position: -19060px 0px; } +.emoji-1F5EC { background-position: -19080px 0px; } +.emoji-1F5ED { background-position: -19100px 0px; } +.emoji-1F5EE { background-position: -19120px 0px; } +.emoji-1F5EF { background-position: -19140px 0px; } +.emoji-1F5F0 { background-position: -19160px 0px; } +.emoji-1F5F1 { background-position: -19180px 0px; } +.emoji-1F5F2 { background-position: -19200px 0px; } +.emoji-1F5F3 { background-position: -19220px 0px; } +.emoji-1F5F4 { background-position: -19240px 0px; } +.emoji-1F5F5 { background-position: -19260px 0px; } +.emoji-1F5F8 { background-position: -19280px 0px; } +.emoji-1F5F9 { background-position: -19300px 0px; } +.emoji-1F5FA { background-position: -19320px 0px; } +.emoji-1F5FB { background-position: -19340px 0px; } +.emoji-1F5FC { background-position: -19360px 0px; } +.emoji-1F5FD { background-position: -19380px 0px; } +.emoji-1F5FE { background-position: -19400px 0px; } +.emoji-1F5FF { background-position: -19420px 0px; } +.emoji-1F600 { background-position: -19440px 0px; } +.emoji-1F601 { background-position: -19460px 0px; } +.emoji-1F602 { background-position: -19480px 0px; } +.emoji-1F603 { background-position: -19500px 0px; } +.emoji-1F604 { background-position: -19520px 0px; } +.emoji-1F605 { background-position: -19540px 0px; } +.emoji-1F606 { background-position: -19560px 0px; } +.emoji-1F607 { background-position: -19580px 0px; } +.emoji-1F608 { background-position: -19600px 0px; } +.emoji-1F609 { background-position: -19620px 0px; } +.emoji-1F60A { background-position: -19640px 0px; } +.emoji-1F60B { background-position: -19660px 0px; } +.emoji-1F60C { background-position: -19680px 0px; } +.emoji-1F60D { background-position: -19700px 0px; } +.emoji-1F60E { background-position: -19720px 0px; } +.emoji-1F60F { background-position: -19740px 0px; } +.emoji-1F610 { background-position: -19760px 0px; } +.emoji-1F611 { background-position: -19780px 0px; } +.emoji-1F612 { background-position: -19800px 0px; } +.emoji-1F613 { background-position: -19820px 0px; } +.emoji-1F614 { background-position: -19840px 0px; } +.emoji-1F615 { background-position: -19860px 0px; } +.emoji-1F616 { background-position: -19880px 0px; } +.emoji-1F617 { background-position: -19900px 0px; } +.emoji-1F618 { background-position: -19920px 0px; } +.emoji-1F619 { background-position: -19940px 0px; } +.emoji-1F61A { background-position: -19960px 0px; } +.emoji-1F61B { background-position: -19980px 0px; } +.emoji-1F61C { background-position: -20000px 0px; } +.emoji-1F61D { background-position: -20020px 0px; } +.emoji-1F61E { background-position: -20040px 0px; } +.emoji-1F61F { background-position: -20060px 0px; } +.emoji-1F620 { background-position: -20080px 0px; } +.emoji-1F621 { background-position: -20100px 0px; } +.emoji-1F622 { background-position: -20120px 0px; } +.emoji-1F623 { background-position: -20140px 0px; } +.emoji-1F624 { background-position: -20160px 0px; } +.emoji-1F625 { background-position: -20180px 0px; } +.emoji-1F626 { background-position: -20200px 0px; } +.emoji-1F627 { background-position: -20220px 0px; } +.emoji-1F628 { background-position: -20240px 0px; } +.emoji-1F629 { background-position: -20260px 0px; } +.emoji-1F62A { background-position: -20280px 0px; } +.emoji-1F62B { background-position: -20300px 0px; } +.emoji-1F62C { background-position: -20320px 0px; } +.emoji-1F62D { background-position: -20340px 0px; } +.emoji-1F62E { background-position: -20360px 0px; } +.emoji-1F62F { background-position: -20380px 0px; } +.emoji-1F630 { background-position: -20400px 0px; } +.emoji-1F631 { background-position: -20420px 0px; } +.emoji-1F632 { background-position: -20440px 0px; } +.emoji-1F633 { background-position: -20460px 0px; } +.emoji-1F634 { background-position: -20480px 0px; } +.emoji-1F635 { background-position: -20500px 0px; } +.emoji-1F636 { background-position: -20520px 0px; } +.emoji-1F637 { background-position: -20540px 0px; } +.emoji-1F638 { background-position: -20560px 0px; } +.emoji-1F639 { background-position: -20580px 0px; } +.emoji-1F63A { background-position: -20600px 0px; } +.emoji-1F63B { background-position: -20620px 0px; } +.emoji-1F63C { background-position: -20640px 0px; } +.emoji-1F63D { background-position: -20660px 0px; } +.emoji-1F63E { background-position: -20680px 0px; } +.emoji-1F63F { background-position: -20700px 0px; } +.emoji-1F640 { background-position: -20720px 0px; } +.emoji-1F641 { background-position: -20740px 0px; } +.emoji-1F642 { background-position: -20760px 0px; } +.emoji-1F645 { background-position: -20780px 0px; } +.emoji-1F646 { background-position: -20800px 0px; } +.emoji-1F647 { background-position: -20820px 0px; } +.emoji-1F648 { background-position: -20840px 0px; } +.emoji-1F649 { background-position: -20860px 0px; } +.emoji-1F64A { background-position: -20880px 0px; } +.emoji-1F64B { background-position: -20900px 0px; } +.emoji-1F64C { background-position: -20920px 0px; } +.emoji-1F64D { background-position: -20940px 0px; } +.emoji-1F64E { background-position: -20960px 0px; } +.emoji-1F64F { background-position: -20980px 0px; } +.emoji-1F680 { background-position: -21000px 0px; } +.emoji-1F681 { background-position: -21020px 0px; } +.emoji-1F682 { background-position: -21040px 0px; } +.emoji-1F683 { background-position: -21060px 0px; } +.emoji-1F684 { background-position: -21080px 0px; } +.emoji-1F685 { background-position: -21100px 0px; } +.emoji-1F686 { background-position: -21120px 0px; } +.emoji-1F687 { background-position: -21140px 0px; } +.emoji-1F688 { background-position: -21160px 0px; } +.emoji-1F689 { background-position: -21180px 0px; } +.emoji-1F68A { background-position: -21200px 0px; } +.emoji-1F68B { background-position: -21220px 0px; } +.emoji-1F68C { background-position: -21240px 0px; } +.emoji-1F68D { background-position: -21260px 0px; } +.emoji-1F68E { background-position: -21280px 0px; } +.emoji-1F68F { background-position: -21300px 0px; } +.emoji-1F690 { background-position: -21320px 0px; } +.emoji-1F691 { background-position: -21340px 0px; } +.emoji-1F692 { background-position: -21360px 0px; } +.emoji-1F693 { background-position: -21380px 0px; } +.emoji-1F694 { background-position: -21400px 0px; } +.emoji-1F695 { background-position: -21420px 0px; } +.emoji-1F696 { background-position: -21440px 0px; } +.emoji-1F697 { background-position: -21460px 0px; } +.emoji-1F698 { background-position: -21480px 0px; } +.emoji-1F699 { background-position: -21500px 0px; } +.emoji-1F69A { background-position: -21520px 0px; } +.emoji-1F69B { background-position: -21540px 0px; } +.emoji-1F69C { background-position: -21560px 0px; } +.emoji-1F69D { background-position: -21580px 0px; } +.emoji-1F69E { background-position: -21600px 0px; } +.emoji-1F69F { background-position: -21620px 0px; } +.emoji-1F6A0 { background-position: -21640px 0px; } +.emoji-1F6A1 { background-position: -21660px 0px; } +.emoji-1F6A2 { background-position: -21680px 0px; } +.emoji-1F6A3 { background-position: -21700px 0px; } +.emoji-1F6A4 { background-position: -21720px 0px; } +.emoji-1F6A5 { background-position: -21740px 0px; } +.emoji-1F6A6 { background-position: -21760px 0px; } +.emoji-1F6A7 { background-position: -21780px 0px; } +.emoji-1F6A8 { background-position: -21800px 0px; } +.emoji-1F6A9 { background-position: -21820px 0px; } +.emoji-1F6AA { background-position: -21840px 0px; } +.emoji-1F6AB { background-position: -21860px 0px; } +.emoji-1F6AC { background-position: -21880px 0px; } +.emoji-1F6AD { background-position: -21900px 0px; } +.emoji-1F6AE { background-position: -21920px 0px; } +.emoji-1F6AF { background-position: -21940px 0px; } +.emoji-1F6B0 { background-position: -21960px 0px; } +.emoji-1F6B1 { background-position: -21980px 0px; } +.emoji-1F6B2 { background-position: -22000px 0px; } +.emoji-1F6B3 { background-position: -22020px 0px; } +.emoji-1F6B4 { background-position: -22040px 0px; } +.emoji-1F6B5 { background-position: -22060px 0px; } +.emoji-1F6B6 { background-position: -22080px 0px; } +.emoji-1F6B7 { background-position: -22100px 0px; } +.emoji-1F6B8 { background-position: -22120px 0px; } +.emoji-1F6B9 { background-position: -22140px 0px; } +.emoji-1F6BA { background-position: -22160px 0px; } +.emoji-1F6BB { background-position: -22180px 0px; } +.emoji-1F6BC { background-position: -22200px 0px; } +.emoji-1F6BD { background-position: -22220px 0px; } +.emoji-1F6BE { background-position: -22240px 0px; } +.emoji-1F6BF { background-position: -22260px 0px; } +.emoji-1F6C0 { background-position: -22280px 0px; } +.emoji-1F6C1 { background-position: -22300px 0px; } +.emoji-1F6C2 { background-position: -22320px 0px; } +.emoji-1F6C3 { background-position: -22340px 0px; } +.emoji-1F6C4 { background-position: -22360px 0px; } +.emoji-1F6C5 { background-position: -22380px 0px; } +.emoji-1F6C6 { background-position: -22400px 0px; } +.emoji-1F6C7 { background-position: -22420px 0px; } +.emoji-1F6C8 { background-position: -22440px 0px; } +.emoji-1F6C9 { background-position: -22460px 0px; } +.emoji-1F6CA { background-position: -22480px 0px; } +.emoji-1F6CB { background-position: -22500px 0px; } +.emoji-1F6CC { background-position: -22520px 0px; } +.emoji-1F6CD { background-position: -22540px 0px; } +.emoji-1F6CE { background-position: -22560px 0px; } +.emoji-1F6CF { background-position: -22580px 0px; } +.emoji-1F6E0 { background-position: -22600px 0px; } +.emoji-1F6E1 { background-position: -22620px 0px; } +.emoji-1F6E2 { background-position: -22640px 0px; } +.emoji-1F6E3 { background-position: -22660px 0px; } +.emoji-1F6E4 { background-position: -22680px 0px; } +.emoji-1F6E5 { background-position: -22700px 0px; } +.emoji-1F6E6 { background-position: -22720px 0px; } +.emoji-1F6E7 { background-position: -22740px 0px; } +.emoji-1F6E8 { background-position: -22760px 0px; } +.emoji-1F6E9 { background-position: -22780px 0px; } +.emoji-1F6EA { background-position: -22800px 0px; } +.emoji-1F6EB { background-position: -22820px 0px; } +.emoji-1F6EC { background-position: -22840px 0px; } +.emoji-1F6F0 { background-position: -22860px 0px; } +.emoji-1F6F1 { background-position: -22880px 0px; } +.emoji-1F6F2 { background-position: -22900px 0px; } +.emoji-1F6F3 { background-position: -22920px 0px; } +.emoji-203C { background-position: -22940px 0px; } +.emoji-2049 { background-position: -22960px 0px; } +.emoji-2122 { background-position: -22980px 0px; } +.emoji-2139 { background-position: -23000px 0px; } +.emoji-2194 { background-position: -23020px 0px; } +.emoji-2195 { background-position: -23040px 0px; } +.emoji-2196 { background-position: -23060px 0px; } +.emoji-2197 { background-position: -23080px 0px; } +.emoji-2198 { background-position: -23100px 0px; } +.emoji-2199 { background-position: -23120px 0px; } +.emoji-21A9 { background-position: -23140px 0px; } +.emoji-21AA { background-position: -23160px 0px; } +.emoji-231A { background-position: -23180px 0px; } +.emoji-231B { background-position: -23200px 0px; } +.emoji-23E9 { background-position: -23220px 0px; } +.emoji-23EA { background-position: -23240px 0px; } +.emoji-23EB { background-position: -23260px 0px; } +.emoji-23EC { background-position: -23280px 0px; } +.emoji-23F0 { background-position: -23300px 0px; } +.emoji-23F3 { background-position: -23320px 0px; } +.emoji-24C2 { background-position: -23340px 0px; } +.emoji-25AA { background-position: -23360px 0px; } +.emoji-25AB { background-position: -23380px 0px; } +.emoji-25B6 { background-position: -23400px 0px; } +.emoji-25C0 { background-position: -23420px 0px; } +.emoji-25FB { background-position: -23440px 0px; } +.emoji-25FC { background-position: -23460px 0px; } +.emoji-25FD { background-position: -23480px 0px; } +.emoji-25FE { background-position: -23500px 0px; } +.emoji-2600 { background-position: -23520px 0px; } +.emoji-2601 { background-position: -23540px 0px; } +.emoji-260E { background-position: -23560px 0px; } +.emoji-2611 { background-position: -23580px 0px; } +.emoji-2614 { background-position: -23600px 0px; } +.emoji-2615 { background-position: -23620px 0px; } +.emoji-261D { background-position: -23640px 0px; } +.emoji-263A { background-position: -23660px 0px; } +.emoji-2648 { background-position: -23680px 0px; } +.emoji-2649 { background-position: -23700px 0px; } +.emoji-264A { background-position: -23720px 0px; } +.emoji-264B { background-position: -23740px 0px; } +.emoji-264C { background-position: -23760px 0px; } +.emoji-264D { background-position: -23780px 0px; } +.emoji-264E { background-position: -23800px 0px; } +.emoji-264F { background-position: -23820px 0px; } +.emoji-2650 { background-position: -23840px 0px; } +.emoji-2651 { background-position: -23860px 0px; } +.emoji-2652 { background-position: -23880px 0px; } +.emoji-2653 { background-position: -23900px 0px; } +.emoji-2660 { background-position: -23920px 0px; } +.emoji-2663 { background-position: -23940px 0px; } +.emoji-2665 { background-position: -23960px 0px; } +.emoji-2666 { background-position: -23980px 0px; } +.emoji-2668 { background-position: -24000px 0px; } +.emoji-267B { background-position: -24020px 0px; } +.emoji-267F { background-position: -24040px 0px; } +.emoji-2693 { background-position: -24060px 0px; } +.emoji-26A0 { background-position: -24080px 0px; } +.emoji-26A1 { background-position: -24100px 0px; } +.emoji-26AA { background-position: -24120px 0px; } +.emoji-26AB { background-position: -24140px 0px; } +.emoji-26BD { background-position: -24160px 0px; } +.emoji-26BE { background-position: -24180px 0px; } +.emoji-26C4 { background-position: -24200px 0px; } +.emoji-26C5 { background-position: -24220px 0px; } +.emoji-26CE { background-position: -24240px 0px; } +.emoji-26D4 { background-position: -24260px 0px; } +.emoji-26EA { background-position: -24280px 0px; } +.emoji-26F2 { background-position: -24300px 0px; } +.emoji-26F3 { background-position: -24320px 0px; } +.emoji-26F5 { background-position: -24340px 0px; } +.emoji-26FA { background-position: -24360px 0px; } +.emoji-26FD { background-position: -24380px 0px; } +.emoji-2702 { background-position: -24400px 0px; } +.emoji-2705 { background-position: -24420px 0px; } +.emoji-2708 { background-position: -24440px 0px; } +.emoji-2709 { background-position: -24460px 0px; } +.emoji-270A { background-position: -24480px 0px; } +.emoji-270B { background-position: -24500px 0px; } +.emoji-270C { background-position: -24520px 0px; } +.emoji-270F { background-position: -24540px 0px; } +.emoji-2712 { background-position: -24560px 0px; } +.emoji-2714 { background-position: -24580px 0px; } +.emoji-2716 { background-position: -24600px 0px; } +.emoji-2728 { background-position: -24620px 0px; } +.emoji-2733 { background-position: -24640px 0px; } +.emoji-2734 { background-position: -24660px 0px; } +.emoji-2744 { background-position: -24680px 0px; } +.emoji-2747 { background-position: -24700px 0px; } +.emoji-274C { background-position: -24720px 0px; } +.emoji-274E { background-position: -24740px 0px; } +.emoji-2753 { background-position: -24760px 0px; } +.emoji-2754 { background-position: -24780px 0px; } +.emoji-2755 { background-position: -24800px 0px; } +.emoji-2757 { background-position: -24820px 0px; } +.emoji-2764 { background-position: -24840px 0px; } +.emoji-2795 { background-position: -24860px 0px; } +.emoji-2796 { background-position: -24880px 0px; } +.emoji-2797 { background-position: -24900px 0px; } +.emoji-27A1 { background-position: -24920px 0px; } +.emoji-27B0 { background-position: -24940px 0px; } +.emoji-27BF { background-position: -24960px 0px; } +.emoji-2934 { background-position: -24980px 0px; } +.emoji-2935 { background-position: -25000px 0px; } +.emoji-2B05 { background-position: -25020px 0px; } +.emoji-2B06 { background-position: -25040px 0px; } +.emoji-2B07 { background-position: -25060px 0px; } +.emoji-2B1B { background-position: -25080px 0px; } +.emoji-2B1C { background-position: -25100px 0px; } +.emoji-2B50 { background-position: -25120px 0px; } +.emoji-2B55 { background-position: -25140px 0px; } +.emoji-3030 { background-position: -25160px 0px; } +.emoji-303D { background-position: -25180px 0px; } +.emoji-3297 { background-position: -25200px 0px; } +.emoji-3299 { background-position: -25220px 0px; } \ No newline at end of file diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 797a0af3720a66f78d2b55040d71e43c3cebd768..9da273a0b6b399a664c35eda53d3efcce6c94581 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -18,7 +18,7 @@ &.affix { position: fixed; - top: 60px; + top: 70px; margin-right: 35px; } } @@ -36,33 +36,12 @@ } .issuable-details { - .issue-title { - margin: 0; - font-size: 23px; - color: #313236; - } - - .description { - margin-top: 6px; - - p:last-child { - margin-bottom: 0; - } - } - section { - border-right: 1px solid #ECEEF1; + border-right: 1px solid $border-white-light; - > .tab-content { + .issuable-discussion { margin-right: 1px; } - - .issue-discussion > .gray-content-block, - > .gray-content-block { - margin-top: 0; - border-top: none; - margin-right: -15px; - } } } @@ -136,21 +115,3 @@ margin-right: 2px; } } - -.issuable-title { - margin: -$gl-padding; - padding: 7px $gl-padding; - margin-bottom: 0px; - border-bottom: 1px solid $border-color; - color: #5c5d5e; - font-size: 16px; - line-height: 42px; - - .author { - color: #5c5d5e; - } - - .issuable-id { - color: #5c5d5e; - } -} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index a652b65502ffaafaa9759b705f6135b53052c76d..a02a3a72e79fc54bd8f1fa865e79c65823f29064 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -141,11 +141,6 @@ form.edit-issue { } } -.issue-closed-by-widget { - padding: 16px 0; - margin: 0px; -} - .issue-form .select2-container { width: 250px !important; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 502e9552acdba8c2b12e573507fd3f71dda7afdc..82effde0bf3b49e8bb8aed35f1c2387292dfc6ea 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -191,7 +191,7 @@ .btn-clipboard { @extend .pull-right; - margin-right: 18px; + margin-right: 20px; margin-top: 5px; position: absolute; right: 0; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index e1a72af00130ea5234072ea58e118c13e1b0800a..d86259f93fbcfe672cf0fab9d84d03ad196a21fe 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -75,17 +75,15 @@ .common-note-form { margin: 0; - background: #F7F8FA; + background: #fff; padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-right: 1px solid $border-color; - border-top: 1px solid $border-color; margin-bottom: -$gl-padding; } .note-form-actions { - background: #F9F9F9; + background: #fff; .note-form-option { margin-top: 8px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4dff87abaa4ec2d556c336014544c62cefcebf25..72b0ed29a698b1d2e95b79c8c654f61784a2165f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -128,7 +128,7 @@ ul.notes { } &:last-child { - border-bottom: none; + border-bottom: 1px solid $border-color; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2ded32dba123746e77ff1d319852ac21f3ad1755..cff3edb7ed26c2a5caa0a062001c5af7d705017f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -91,21 +91,83 @@ } } - .input-group { + .git-clone-holder { display: inline-table; position: relative; - top: 17px; } .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; + .count-buttons { + display: block; + margin-bottom: 12px; + } + .btn { @include btn-gray; - + text-transform: none; + } + .count-with-arrow { + display: inline-block; + position: relative; + margin-left: 4px; + + .arrow { + &:before { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 50%; + left: 0; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: #dce0e5; + } + + &:after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 50%; + left: 1px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: #FFF; + } + } .count { + @include btn-gray; display: inline-block; + background: white; + border-radius: 2px; + border-width: 1px; + border-style: solid; + font-size: 13px; + font-weight: 600; + line-height: 20px; + padding: 11px 16px; + letter-spacing: .4px; + padding: 10px; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; + white-space: nowrap; + margin: 0 11px 0px 4px; + + &:hover { + background: #FFF; + } } } } @@ -125,6 +187,13 @@ margin-right: 45px; } + .clone-options { + display: table-cell; + a.btn { + width: 100%; + } + } + .form-control { cursor: auto; @extend .monospace; @@ -335,6 +404,38 @@ ul.nav.nav-projects-tabs { } } +.top-area { + border-bottom: 1px solid #EEE; + margin: 0 -16px; + padding: 0 $gl-padding; + + ul.left-top-menu { + display: inline-block; + width: 50%; + margin-bottom: 0px; + border-bottom: none; + } + + .projects-search-form { + width: 50%; + display: inline-block; + float: right; + padding-top: 7px; + text-align: right; + + .btn-green { + margin-top: -2px; + margin-left: 10px; + } + } + + @media (max-width: $screen-xs-max) { + .projects-search-form { + padding-top: 15px; + } + } +} + .fork-namespaces { .fork-thumbnail { text-align: center; @@ -412,11 +513,18 @@ pre.light-well { .projects-search-form { margin: -$gl-padding; - background-color: #f8fafc; padding: $gl-padding; margin-bottom: 0px; - border-top: 1px solid #e7e9ed; - border-bottom: 1px solid #e7e9ed; + + input { + display: inline-block; + width: calc(100% - 151px); + } + + .btn { + display: inline-block; + width: 135px; + } } .git-empty { diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index a7d3b2197f15f82636018d1e00e6ad6b6792256d..4b6ef035673b595f1b835a7444e40abc0cb70ae4 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -35,3 +35,20 @@ border-color: $gl-warning; } } + +.ci-status-icon-success { + @extend .cgreen; +} +.ci-status-icon-failed { + @extend .cred; +} +.ci-status-icon-running, +.ci-status-icon-pending { + // These are standard text color +} +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found, +.ci-status-icon-skipped { + @extend .cgray; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9dd16f8c7353a7b8304efa6ae265ce89bcb23332..10e736fd362248e9e9f0aa6e3f07fbd11202742a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -49,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :default_branch_protection, :signup_enabled, :signin_enabled, + :require_two_factor_authentication, + :two_factor_grace_period, :gravatar_enabled, :twitter_sharing_enabled, :sign_in_text, @@ -65,6 +67,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :user_oauth_applications, :shared_runners_enabled, :max_artifacts_size, + :metrics_enabled, + :metrics_host, + :metrics_port, + :metrics_username, + :metrics_password, + :metrics_pool_size, + :metrics_timeout, + :metrics_method_call_threshold, + :recaptcha_enabled, + :recaptcha_site_key, + :recaptcha_private_key, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index d28614731f9211cf66fb1ef3af5e76a2c3fe972d..e383fe38ea68352c5ba172a396a978f805a27527 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -1,6 +1,21 @@ class Admin::IdentitiesController < Admin::ApplicationController before_action :user - before_action :identity, except: :index + before_action :identity, except: [:index, :new, :create] + + def new + @identity = Identity.new + end + + def create + @identity = Identity.new(identity_params) + @identity.user_id = user.id + + if @identity.save + redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.' + else + render :new + end + end def index @identities = @user.identities diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d182e8eb04d92fded1152636459a9be917d82c1..d9a37a4d45f8bccd7961e0e731ecd46f3b95eef0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,8 +10,10 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_token! before_action :authenticate_user! + before_action :validate_user_service_ticket! before_action :reject_blocked! before_action :check_password_expiration + before_action :check_2fa_requirement before_action :ldap_security_check before_action :default_headers before_action :add_gon_variables @@ -202,12 +204,32 @@ class ApplicationController < ActionController::Base end end + def validate_user_service_ticket! + return unless signed_in? && session[:service_tickets] + + valid = session[:service_tickets].all? do |provider, ticket| + Gitlab::OAuth::Session.valid?(provider, ticket) + end + + unless valid + session[:service_tickets] = nil + sign_out current_user + redirect_to new_user_session_path + end + end + def check_password_expiration if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end + def check_2fa_requirement + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? + redirect_to new_profile_two_factor_auth_path + end + end + def ldap_security_check if current_user && current_user.requires_ldap_check? unless Gitlab::LDAP::Access.allowed?(current_user) @@ -342,6 +364,23 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('git') end + def two_factor_authentication_required? + current_application_settings.require_two_factor_authentication + end + + def two_factor_grace_period + current_application_settings.two_factor_grace_period + end + + def two_factor_grace_period_expired? + date = current_user.otp_grace_period_started_at + date && (date + two_factor_grace_period.hours) < Time.current + end + + def skip_two_factor? + session[:skip_tfa] && session[:skip_tfa] > Time.current + end + def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 7ed78ff8e989492249818665fc8ae1a7c6ea5784..e782a51e7eb93dce8ca605dd2236cd749a272119 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -19,8 +19,10 @@ module Ci @error = e.message @status = false rescue - @error = "Undefined error" + @error = 'Undefined error' @status = false + ensure + render :show end end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..62127a090817e01355ec314399db321c0d378a56 --- /dev/null +++ b/app/controllers/concerns/creates_commit.rb @@ -0,0 +1,103 @@ +module CreatesCommit + extend ActiveSupport::Concern + + def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) + set_commit_variables + + commit_params = @commit_params.merge( + source_project: @project, + source_branch: @ref, + target_branch: @target_branch + ) + + result = service.new(@tree_edit_project, current_user, commit_params).execute + + if result[:status] == :success + flash[:notice] = success_notice || "Your changes have been successfully committed." + + if create_merge_request? + success_path = new_merge_request_path + target = different_project? ? "project" : "branch" + flash[:notice] << " You can now submit a merge request to get this change into the original #{target}." + end + + respond_to do |format| + format.html { redirect_to success_path } + format.json { render json: { message: "success", filePath: success_path } } + end + else + flash[:alert] = result[:message] + respond_to do |format| + format.html do + if failure_view + render failure_view + else + redirect_to failure_path + end + end + format.json { render json: { message: "failed", filePath: failure_path } } + end + end + end + + def authorize_edit_tree! + return if can?(current_user, :push_code, project) + return if current_user && current_user.already_forked?(project) + + access_denied! + end + + private + + def new_merge_request_path + new_namespace_project_merge_request_path( + @mr_source_project.namespace, + @mr_source_project, + merge_request: { + source_project_id: @mr_source_project.id, + target_project_id: @mr_target_project.id, + source_branch: @mr_source_branch, + target_branch: @mr_target_branch + } + ) + end + + def different_project? + @mr_source_project != @mr_target_project + end + + def different_branch? + @mr_source_branch != @mr_target_branch || different_project? + end + + def create_merge_request? + params[:create_merge_request].present? && different_branch? + end + + def set_commit_variables + @mr_source_branch = @target_branch + + if can?(current_user, :push_code, @project) + # Edit file in this project + @tree_edit_project = @project + @mr_source_project = @project + + if @project.forked? + # Merge request from this project to fork origin + @mr_target_project = @project.forked_from_project + @mr_target_branch = @mr_target_project.repository.root_ref + else + # Merge request to this project + @mr_target_project = @project + @mr_target_branch = @ref + end + else + # Edit file in fork + @tree_edit_project = current_user.fork_of(@project) + # Merge request from fork to this project + @mr_source_project = @tree_edit_project + @mr_target_project = @project + @mr_target_branch = @mr_target_project.repository.root_ref + end + end +end diff --git a/app/controllers/concerns/creates_merge_request_for_commit.rb b/app/controllers/concerns/creates_merge_request_for_commit.rb deleted file mode 100644 index c75278221585a95a18e591f63c1e7839727ace7a..0000000000000000000000000000000000000000 --- a/app/controllers/concerns/creates_merge_request_for_commit.rb +++ /dev/null @@ -1,28 +0,0 @@ -module CreatesMergeRequestForCommit - extend ActiveSupport::Concern - - def new_merge_request_path - if @project.forked? - target_project = @project.forked_from_project || @project - target_branch = target_project.repository.root_ref - else - target_project = @project - target_branch = @ref - end - - new_namespace_project_merge_request_path( - @project.namespace, - @project, - merge_request: { - source_project_id: @project.id, - target_project_id: target_project.id, - source_branch: @new_branch, - target_branch: target_branch - } - ) - end - - def create_merge_request? - params[:create_merge_request] && @new_branch != @ref - end -end diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index f4354c6d8cad9665e6cbb4cd69a6191338202915..b3594d82530b09d06bfb97e2ec7189b42bf98313 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -1,6 +1,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController def index - @snippets = SnippetsFinder.new.execute(current_user, + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_user, user: current_user, scope: params[:scope] diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f809fa7500a09fd37a547238198987e80501f5ea..4cad98b8e98bdd7044ed442ea0622418e14f016a 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,6 +1,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController - protect_from_forgery except: [:kerberos, :saml] + protect_from_forgery except: [:kerberos, :saml, :cas3] Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do @@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController render 'errors/omniauth_error', layout: "errors", status: 422 end + def cas3 + ticket = params['ticket'] + if ticket + handle_service_ticket oauth['provider'], ticket + end + handle_omniauth + end + private def handle_omniauth @@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to new_user_session_path end + def handle_service_ticket provider, ticket + Gitlab::OAuth::Session.create provider, ticket + session[:service_tickets] ||= {} + session[:service_tickets][provider] = ticket + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index e6b99be37fb7fab9bcf2190321a3df0e446b050f..6e91d9b4ad96791adb21a9288b2ac54857d63688 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,8 +1,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController + skip_before_action :check_2fa_requirement + def new unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) - current_user.save! + end + + unless current_user.otp_grace_period_started_at && two_factor_grace_period + current_user.otp_grace_period_started_at = Time.current + end + + current_user.save! if current_user.changed? + + if two_factor_grace_period_expired? + flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.' + else + grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}." end @qr_code = build_qr_code @@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController redirect_to profile_account_path end + def skip + if two_factor_grace_period_expired? + redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' + else + session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + redirect_to root_path + end + end + private def build_qr_code diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 6216368293637cbd528cb589f0f25e573cf785c7..c56a3497bb2d096cb00231fa979148c0c22c8037 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,7 +1,7 @@ # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath - include CreatesMergeRequestForCommit + include CreatesCommit include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path @@ -9,21 +9,21 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:destroy, :create] + before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy] before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] - before_action :after_edit_path, only: [:edit, :update] def new commit unless @repository.empty? end def create - create_commit(Files::CreateService, success_path: after_create_path, + create_commit(Files::CreateService, success_notice: "The file has been successfully created.", + success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)), failure_view: :new, failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end @@ -36,6 +36,14 @@ class Projects::BlobController < Projects::ApplicationController end def update + after_edit_path = + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "#file-path-#{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) + end + create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) @@ -50,15 +58,10 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - result = Files::DeleteService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - redirect_to after_destroy_path - else - flash[:alert] = result[:message] - render :show - end + create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", + success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), + failure_view: :show, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def diff @@ -108,74 +111,13 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def create_commit(service, success_path:, failure_view:, failure_path:) - result = service.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - respond_to do |format| - format.html { redirect_to success_path } - format.json { render json: { message: "success", filePath: success_path } } - end - else - flash[:alert] = result[:message] - respond_to do |format| - format.html { render failure_view } - format.json { render json: { message: "failed", filePath: failure_path } } - end - end - end - - def after_create_path - @after_create_path ||= - if create_merge_request? - new_merge_request_path - else - namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path)) - end - end - - def after_edit_path - @after_edit_path ||= - if create_merge_request? - new_merge_request_path - elsif from_merge_request && @new_branch == @ref - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - else - namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path)) - end - end - - def after_destroy_path - @after_destroy_path ||= - if create_merge_request? - new_merge_request_path - else - namespace_project_tree_path(@project.namespace, @project, @new_branch) - end - end - def from_merge_request # If blob edit was initiated from merge request page @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) end - def sanitized_new_branch_name - sanitize(strip_tags(params[:new_branch])) - end - def editor_variables - @current_branch = @ref - - @new_branch = - if params[:new_branch].present? - sanitized_new_branch_name - elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref) - @ref - else - @repository.next_patch_branch - end + @target_branch = params[:target_branch] @file_path = if action_name.to_s == 'create' @@ -194,8 +136,6 @@ class Projects::BlobController < Projects::ApplicationController @commit_params = { file_path: @file_path, - current_branch: @current_branch, - target_branch: @new_branch, commit_message: params[:commit_message], file_content: params[:content], file_content_encoding: params[:encoding] diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 58fb946dbc24c8a3a06e663f9b9deddf38dde2fb..04a88990bf4d68557cac40687203cdaca2188d6d 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -9,7 +9,7 @@ class Projects::CommitsController < Projects::ApplicationController def show @repo = @project.repository - @limit, @offset = (params[:limit] || 40), (params[:offset] || 0) + @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i @commits = @repo.commits(@ref, @path, @limit, @offset) @note_counts = project.notes.where(commit_id: @commits.map(&:id)). diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 8a785076bb7be6e88c8b0f48de2214ddb48e9bb0..750181f0c1932257d9e60fa465578f986a070d31 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -10,19 +10,35 @@ class Projects::ForksController < Projects::ApplicationController def create namespace = Namespace.find(params[:namespace_key]) - @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute + + @forked_project = namespace.projects.find_by(path: project.path) + @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project + + @forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute if @forked_project.saved? && @forked_project.forked? if @forked_project.import_in_progress? - redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project) + redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project, continue: continue_params) else - redirect_to( - namespace_project_path(@forked_project.namespace, @forked_project), - notice: 'Project was successfully forked.' - ) + if continue_params + redirect_to continue_params[:to], notice: continue_params[:notice] + else + redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked." + end end else render :error end end + + private + + def continue_params + continue_params = params[:continue] + if continue_params + continue_params.permit(:to, :notice, :notice_now) + else + nil + end + end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index fb8788f0818bbbd7d4b022b1dab647ec45035460..8d8035ef5ff9a993b161358a66f1f13e80016367 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,7 +1,7 @@ class Projects::ImportsController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :require_no_repo + before_action :require_no_repo, except: :show before_action :redirect_if_progress, except: :show def new @@ -24,21 +24,36 @@ class Projects::ImportsController < Projects::ApplicationController end def show - unless @project.import_in_progress? - if @project.import_finished? - redirect_to(project_path(@project)) and return + if @project.repository_exists? || @project.import_finished? + if continue_params + redirect_to continue_params[:to], notice: continue_params[:notice] else - redirect_to(new_namespace_project_import_path(@project.namespace, - @project)) and return + redirect_to project_path(@project), notice: "The project was successfully forked." end + elsif @project.import_failed? + redirect_to new_namespace_project_import_path(@project.namespace, @project) + else + if continue_params && continue_params[:notice_now] + flash.now[:notice] = continue_params[:notice_now] + end + # Render end end private + def continue_params + continue_params = params[:continue] + if continue_params + continue_params.permit(:to, :notice, :notice_now) + else + nil + end + end + def require_no_repo if @project.repository_exists? && !@project.import_in_progress? - redirect_to(namespace_project_path(@project.namespace, @project)) and return + redirect_to(namespace_project_path(@project.namespace, @project)) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index fffd90d87eb9c0885d45482260ffc249d146f47c..ab5c953189cfa6d86123fe9d8ac945a1b3903365 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] - before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request @@ -153,11 +153,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_check - if @merge_request.unchecked? - @merge_request.check_if_can_be_merged - end - - closes_issues + @merge_request.check_if_can_be_merged if @merge_request.unchecked? render partial: "projects/merge_requests/widget/show.html.haml", layout: false end @@ -178,7 +174,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active? + if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) .execute(@merge_request) @status = :merge_when_build_succeeds @@ -299,6 +295,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_widget_vars @ci_commit = @merge_request.ci_commit + closes_issues end def invalid_mr diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index ae6e9f6fd3857dd51a88b02f37fa46abb3955c85..6f1e186d4084d4eb4729a74b989785c48115ad6a 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -69,7 +69,7 @@ class Projects::NotesController < Projects::ApplicationController data = { author: current_user, is_award: true, - note: note_params[:note].gsub(":", '') + note: note_params[:note].delete(":") } note = noteable.notes.find_by(data) @@ -139,7 +139,6 @@ class Projects::NotesController < Projects::ApplicationController discussion_id: note.discussion_id, html: note_to_html(note), award: note.is_award, - emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", note: note.note, discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 6b52eccebf73a6f49406052ad28994b82324a39c..e49259c34b6d6c66be68593e72acc969f443695b 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -21,7 +21,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController if protected_branch && protected_branch.update_attributes( - developers_can_push: params[:developers_can_push] + developers_can_push: params[:developers_can_push] ) respond_to do |format| diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 6e7590260ff0e3bfa6f88fc706e54054b0b14c32..8b2577aebe1e537dc368cac0598bfc1154650c30 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -1,5 +1,5 @@ class Projects::ServicesController < Projects::ApplicationController - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain, + ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :drone_url, :build_type, @@ -10,7 +10,8 @@ class Projects::ServicesController < Projects::ApplicationController :notify_only_broken_builds, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] + :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, + :jira_issue_transition_id] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 8f272ad1281fef7306c646248ff67acf1f18465d..cb3ed0f6f9c25dabfb05247f6002bf2478330a3d 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,14 +1,14 @@ # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath - include CreatesMergeRequestForCommit + include CreatesCommit include ActionView::Helpers::SanitizeHelper before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars before_action :assign_dir_vars, only: [:create_dir] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create_dir] + before_action :authorize_edit_tree!, only: [:create_dir] def show return render_404 unless @repository.commit(@ref) @@ -34,44 +34,20 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? - begin - result = Files::CreateDirService.new(@project, current_user, @commit_params).execute - message = result[:message] - rescue => e - message = e.to_s - end - - if result && result[:status] == :success - flash[:notice] = "The directory has been successfully created" - respond_to do |format| - format.html { redirect_to after_create_dir_path } - end - else - flash[:alert] = message - respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) } - end - end + create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", + success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)), + failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) end private def assign_dir_vars - @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref + @target_branch = params[:target_branch] + @dir_name = File.join(@path, params[:dir_name]) @commit_params = { file_path: @dir_name, - current_branch: @ref, - target_branch: @new_branch, commit_message: params[:commit_message], } end - - def after_create_dir_path - if create_merge_request? - new_merge_request_path - else - namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) - end - end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index bf5e25ff89514d0da9e62bdc76aa1cdc392ab52a..3004722bce033664de15775ab47227daf3e02f5b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -171,14 +171,14 @@ class ProjectsController < ApplicationController @project.reload render json: { - html: view_to_html_string("projects/buttons/_star") + star_count: @project.star_count } end def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) ext.analyze(text) render json: { diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3b3dc86cb6852d64ee3855c0ebd43b609f8c519c..c48175a4c5ab25fb73277740ccab91e9e2df1bef 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,10 +1,21 @@ class RegistrationsController < Devise::RegistrationsController before_action :signup_enabled? + include Recaptcha::Verify def new redirect_to(new_user_session_path) end + def create + if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + super + else + flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." + flash.delete :recaptcha_error + render action: 'new' + end + end + def destroy DeleteUserService.new(current_user).execute(current_user) @@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController def sign_up_params params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) end + + def resource_name + :user + end + + def resource + @resource ||= User.new(sign_up_params) + end + + def devise_mapping + @devise_mapping ||= Devise.mappings[:user] + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1b60d3e27d0ae170a410d8ab43b9109b5e66695d..825f85199bef03a33d4daa608bf0f5c38c88c06d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,9 +1,11 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor + include Recaptcha::ClientHelper prepend_before_action :authenticate_with_two_factor, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] + before_action :load_recaptcha def new if Gitlab.config.ldap.enabled @@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController User.find(session[:otp_user_id]) end end - + def store_redirect_path redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes') @@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController provider = Gitlab.config.omniauth.auto_sign_in_with_provider return unless provider.present? - # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is - # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer # to do nothing to prevent redirection loops with certain Omniauth providers. return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') - + # Prevent alert from popping up on the first page shown after authentication. - flash[:alert] = nil - + flash[:alert] = nil + redirect_to user_omniauth_authorize_path(provider.to_sym) end @@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController AuditEventService.new(user, user, options). for_authentication.security_event end + + def load_recaptcha + Gitlab::Recaptcha.load_configurations! + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 21f962df206d5814d5bf4af0551e0c4ba6412119..f7f7a1a02d3ef00d3ce475c35fa23f95542acbaa 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -61,7 +61,7 @@ module ApplicationHelper options[:class] ||= '' options[:class] << ' identicon' bg_key = project.id % 7 - style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" + style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555" content_tag(:div, class: options[:class], style: style) do project.name[0, 1].upcase @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email) + user = User.find_by(email: user_or_email.downcase) end if user @@ -204,12 +204,16 @@ module ApplicationHelper # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago", + class: "#{html_class} js-timeago js-timeago-pending", datetime: time.getutc.iso8601, title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement, container: 'body' } - element += javascript_tag "$('.js-timeago').last().timeago()" unless skip_js + unless skip_js + element << javascript_tag( + "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" + ) + end element end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 2c81ea1623c52f9945519e121e0df1f839926ec1..0cfc0565e84fb50836609e58c4143cfe2ae2ef45 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -50,5 +50,17 @@ module AuthHelper current_user.identities.exists?(provider: provider.to_s) end + def two_factor_skippable? + current_application_settings.require_two_factor_authentication && + !current_user.two_factor_enabled && + current_application_settings.two_factor_grace_period && + !two_factor_grace_period_expired? + end + + def two_factor_grace_period_expired? + current_user.otp_grace_period_started_at && + (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current + end + extend self end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 68e5d5be600edcff277409109b8a7c020bcdcea7..d31d4cde08f4dcb2579d2fd582c50111401e9bca 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -22,32 +22,90 @@ module BlobHelper %w(credits changelog news copying copyright license authors) end - def edit_blob_link(project, ref, path, options = {}) - blob = - begin - project.repository.blob_at(ref, path) - rescue - nil - end - - return unless blob && blob.text? && blob_editable?(blob) - - text = 'Edit' - after = options[:after] || '' + def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) + return unless current_user + + blob = project.repository.blob_at(ref, path) rescue nil + + return unless blob && blob_text_viewable?(blob) + from_mr = options[:from_merge_request_id] link_opts = {} link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - link_to(text, - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - link_opts), - class: cls - ) + after.html_safe + + edit_path = namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts) + + if !on_top_of_branch? + button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } + elsif can_edit_blob?(blob) + link_to "Edit", edit_path, class: 'btn btn-small' + elsif can?(current_user, :fork_project, project) + continue_params = { + to: edit_path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to "Edit", fork_path, class: 'btn btn-small', method: :post + end + end + + def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + return unless current_user + + blob = project.repository.blob_at(ref, path) rescue nil + + return unless blob + + if !on_top_of_branch? + button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } + elsif blob.lfs_pointer? + button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } + elsif can_edit_blob?(blob) + button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' + elsif can?(current_user, :fork_project, project) + continue_params = { + to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to #{action} this file again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post + end + end + + def replace_blob_link(project = @project, ref = @ref, path = @path) + modify_file_link( + project, + ref, + path, + label: "Replace", + action: "replace", + btn_class: "default", + modal_type: "upload" + ) + end + + def delete_blob_link(project = @project, ref = @ref, path = @path) + modify_file_link( + project, + ref, + path, + label: "Delete", + action: "delete", + btn_class: "remove", + modal_type: "remove" + ) end - def blob_editable?(blob, project = @project, ref = @ref) - !blob.lfs_pointer? && allowed_tree_edit?(project, ref) + def can_edit_blob?(blob, project = @project, ref = @ref) + !blob.lfs_pointer? && can_edit_tree?(project, ref) end def leave_edit_message @@ -70,7 +128,7 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_viewable?(blob) + def blob_text_viewable?(blob) blob && blob.text? && !blob.lfs_pointer? end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 313b6dde9102795fda16d8e9eb4e1d322732a76d..ec0e3f409c1596e03e5f344748ea562ddaca4902 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -10,8 +10,8 @@ module ButtonHelper # # => "<button class='...' data-clipboard-text='Foo'>...</button>" # # # Define the target element - # clipboard_button(clipboard_target: "#foo") - # # => "<button class='...' data-clipboard-target='#foo'>...</button>" + # clipboard_button(clipboard_target: "div#foo") + # # => "<button class='...' data-clipboard-target='div#foo'>...</button>" # # See http://clipboardjs.com/#usage def clipboard_button(data = {}) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8554074d6199dda91a9f41c85aa176e0ffe21f14..d8bee21c82e95753236f6b6701ed0249411f7060 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -12,19 +12,6 @@ module CiStatusHelper ci_label_for_status(ci_commit.status) end - def ci_status_color(ci_commit) - case ci_commit.status - when 'success' - 'green' - when 'failed' - 'red' - when 'running', 'pending' - 'yellow' - else - 'gray' - end - end - def ci_status_with_icon(status) content_tag :span, class: "ci-status ci-#{status}" do ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) @@ -56,12 +43,11 @@ module CiStatusHelper end def render_ci_status(ci_commit) - link_to ci_status_path(ci_commit), - class: "ci-status-link c#{ci_status_color(ci_commit)}", + link_to ci_status_icon(ci_commit), + ci_status_path(ci_commit), + class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_status_label(ci_commit)}", - data: { toggle: 'tooltip', placement: 'left' } do - ci_status_icon(ci_commit) - end + data: { toggle: 'tooltip', placement: 'left' } end def no_runners_for_project?(project) diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb index 838b85afdfe573e1d605c784fe47b35731aa9894..1f3401f290637587be4ee512738b864885220569 100644 --- a/app/helpers/external_wiki_helper.rb +++ b/app/helpers/external_wiki_helper.rb @@ -1,7 +1,7 @@ module ExternalWikiHelper def get_project_wiki_path(project) external_wiki_service = project.services. - select { |service| service.to_param == 'external_wiki' }.first + find { |service| service.to_param == 'external_wiki' } if external_wiki_service.present? && external_wiki_service.active? external_wiki_service.properties['external_wiki_url'] else diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index a0cf3dc084398fb165f9919af8003c5e76c596a4..ca41657cec152fbdc6f7f90fa29b09cc7a5f6aec 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -65,7 +65,8 @@ module GitlabMarkdownHelper end def asciidoc(text) - Gitlab::Asciidoc.render(text, + Gitlab::Asciidoc.render( + text, project: @project, current_user: (current_user if defined?(current_user)), diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index cdf7038b2f27770d97b1b830a9011932d471be30..80e2741b09a07795b4678a3346859b0fbbe07495 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -57,18 +57,22 @@ module IssuesHelper options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) end - def issue_box_class(item) + def status_box_class(item) if item.respond_to?(:expired?) && item.expired? - 'issue-box-expired' + 'status-box-expired' elsif item.respond_to?(:merged?) && item.merged? - 'issue-box-merged' + 'status-box-merged' elsif item.closed? - 'issue-box-closed' + 'status-box-closed' else - 'issue-box-open' + 'status-box-open' end end + def issue_button_visibility(issue, closed) + return 'hidden' if issue.closed? == closed + end + def issue_to_atom(xml, issue) xml.entry do xml.id namespace_project_issue_url(issue.project.namespace, @@ -94,11 +98,14 @@ module IssuesHelper end.sort.to_sentence(last_word_connector: ', or ') end - def url_to_emoji(name) - emoji_path = ::AwardEmoji.path_to_emoji_image(name) - url_to_image(emoji_path) - rescue StandardError - "" + def emoji_icon(name, unicode = nil, aliases = []) + unicode ||= Emoji.emoji_filename(name) + + content_tag :div, "", + class: "icon emoji-icon emoji-#{unicode}", + "data-emoji" => name, + "data-aliases" => aliases.join(" "), + "data-unicode-name" => unicode end def emoji_author_list(notes, current_user) @@ -109,10 +116,6 @@ module IssuesHelper list.join(", ") end - def emoji_list - ::AwardEmoji::EMOJI_LIST - end - def note_active_class(notes, current_user) if current_user && notes.pluck(:author_id).include?(current_user.id) "active" @@ -121,6 +124,18 @@ module IssuesHelper end end + def awards_sort(awards) + awards.sort_by do |award, notes| + if award == "thumbsup" + 0 + elsif award == "thumbsdown" + 1 + else + 2 + end + end.to_h + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 6c32647594d5a0642d70f52ed57cb93b5989badb..1dd07a2a2204b0368671f57e3b89b05b82432477 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -27,7 +27,16 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + return nil unless build_url + + parsed_url = URI.parse(build_url) + + unless parsed_url.userinfo.blank? + parsed_url.userinfo = '' + end + + parsed_url.to_s end def merge_path_description(merge_request, separator) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 9bf750124b25085361aafff6c982916ef10a2c27..791cb9e50bd013b738d5d59c22a17ea098a6816b 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -8,6 +8,80 @@ module PageLayoutHelper @page_title.join(" \u00b7 ") end + # Define or get a description for the current page + # + # description - String (default: nil) + # + # If this helper is called multiple times with an argument, only the last + # description will be returned when called without an argument. Descriptions + # have newlines replaced with spaces and all HTML tags are sanitized. + # + # Examples: + # + # page_description # => "GitLab Community Edition" + # page_description("Foo") + # page_description # => "Foo" + # + # page_description("<b>Bar</b>\nBaz") + # page_description # => "Bar Baz" + # + # Returns an HTML-safe String. + def page_description(description = nil) + @page_description ||= page_description_default + + if description.present? + @page_description = description.squish + else + sanitize(@page_description, tags: []).truncate_words(30) + end + end + + # Default value for page_description when one hasn't been defined manually by + # a view + def page_description_default + if @project + @project.description || brand_title + else + brand_title + end + end + + def page_image + default = image_url('gitlab_logo.png') + + if @project + @project.avatar_url || default + elsif @user + avatar_icon(@user) + else + default + end + end + + # Define or get attributes to be used as Twitter card metadata + # + # map - Hash of label => data pairs. Keys become labels, values become data + # + # Raises ArgumentError if given more than two attributes + def page_card_attributes(map = {}) + raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2 + + @page_card_attributes ||= {} + @page_card_attributes = map.reject { |_,v| v.blank? } if map.present? + @page_card_attributes + end + + def page_card_meta_tags + tags = '' + + page_card_attributes.each_with_index do |pair, i| + tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0]) + tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1]) + end + + tags.html_safe + end + def header_title(title = nil, title_url = nil) if title @header_title = title diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d061136b7b856a2cec99a073459b1d045561e0b3..77ba612548a11785dbb214bbd2e8026e9c879946 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -105,6 +105,14 @@ module ProjectsHelper end end + def user_max_access_in_project(user_id, project) + level = project.team.max_member_access(user_id) + + if level + Gitlab::Access.options_with_owner.key(level) + end + end + private def get_project_nav_tabs(project, current_user) @@ -277,14 +285,6 @@ module ProjectsHelper end end - def user_max_access_in_project(user, project) - level = project.team.max_member_access(user) - - if level - Gitlab::Access.options_with_owner.key(level) - end - end - def leave_project_message(project) "Are you sure you want to leave \"#{project.name}\" project?" end @@ -330,10 +330,9 @@ module ProjectsHelper def filename_path(project, filename) if project && blob = project.repository.send(filename) namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - blob.name) + project.namespace, + project, + tree_join(project.default_branch, blob.name) ) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 886a1e734b5c1ca71acf9bd175ae4134ea04d738..2ad7c80dae0fe5379a9f074f3fcdf6a816e128f5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -50,24 +50,49 @@ module TreeHelper project.repository.branch_names.include?(ref) end - def allowed_tree_edit?(project = nil, ref = nil) + def can_edit_tree?(project = nil, ref = nil) project ||= @project ref ||= @ref + return false unless on_top_of_branch?(project, ref) - can?(current_user, :push_code, project) + can?(current_user, :push_code, project) || + (current_user && current_user.already_forked?(project)) end def tree_edit_branch(project = @project, ref = @ref) - if allowed_tree_edit?(project, ref) - if can_push_branch?(project, ref) - ref - else - project.repository.next_patch_branch - end + return unless can_edit_tree?(project, ref) + + if can_push_branch?(project, ref) + ref + else + project = tree_edit_project(project) + project.repository.next_patch_branch + end + end + + def tree_edit_project(project = @project) + if can?(current_user, :push_code, project) + project + elsif current_user && current_user.already_forked?(project) + current_user.fork_of(project) end end + def edit_in_new_fork_notice_now + "You're not allowed to make changes to this project directly." + + " A fork of this project is being created that you can make changes in, so you can submit a merge request." + end + + def edit_in_new_fork_notice + "You're not allowed to make changes to this project directly." + + " A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + def commit_in_fork_help + "A new branch will be created in your fork and a new merge request will be started." + end + def tree_breadcrumbs(tree, max_links = 2) if @path.present? part_path = "" @@ -79,7 +104,7 @@ module TreeHelper part_path = File.join(part_path, part) unless part_path.empty? part_path = part if part_path.empty? - next unless parts.last(2).include?(part) if parts.count > max_links + next if parts.count > max_links && !parts.last(2).include?(part) yield(part, tree_join(@ref, part_path)) end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 2e69ce923a2f9438e8fc788be8447b28f72ce838..71d33b445c22bea272c664e004ef0f01401ea4db 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -69,7 +69,6 @@ module VisibilityLevelHelper def skip_level?(form_model, level) form_model.is_a?(Project) && - form_model.forked? && - !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + !form_model.visibility_level_allowed?(level) end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 9beb0de68f3b8ebdd7989e682fbd0bd126505d3e..3bbdd9cee76c2761718bc65259cbebfd5f8c65bf 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -17,7 +17,7 @@ class Notify < BaseMailer subject: subject, body: body.html_safe, content_type: 'text/html' - ) + ) end # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", diff --git a/app/models/ability.rb b/app/models/ability.rb index cd5ae0fb0fd3c99182320d605c7e24bc5904a2b9..1b3ee757040515250134b79afe752fc30883f16a 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -132,14 +132,14 @@ class Ability end def public_project_rules - project_guest_rules + [ + @public_project_rules ||= project_guest_rules + [ :download_code, :fork_project ] end def project_guest_rules - [ + @project_guest_rules ||= [ :read_project, :read_wiki, :read_issue, @@ -157,7 +157,7 @@ class Ability end def project_report_rules - project_guest_rules + [ + @project_report_rules ||= project_guest_rules + [ :create_commit_status, :read_commit_statuses, :download_code, @@ -170,7 +170,7 @@ class Ability end def project_dev_rules - project_report_rules + [ + @project_dev_rules ||= project_report_rules + [ :admin_merge_request, :create_merge_request, :create_wiki, @@ -181,7 +181,7 @@ class Ability end def project_archived_rules - [ + @project_archived_rules ||= [ :create_merge_request, :push_code, :push_code_to_protected_branches, @@ -191,7 +191,7 @@ class Ability end def project_master_rules - project_dev_rules + [ + @project_master_rules ||= project_dev_rules + [ :push_code_to_protected_branches, :update_project_snippet, :update_merge_request, @@ -206,7 +206,7 @@ class Ability end def project_admin_rules - project_master_rules + [ + @project_admin_rules ||= project_master_rules + [ :change_namespace, :change_visibility_level, :rename_project, @@ -332,7 +332,7 @@ class Ability end if snippet.public? || snippet.internal? - rules << :read_personal_snippet + rules << :read_personal_snippet end rules diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index faa0bdf840b53d5f5b4359b1e5686f43e45cbb0a..be69d317d7306edd3577c51e6a95f5600e503fdb 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -2,32 +2,34 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string(255) +# require_two_factor_authentication :boolean default(TRUE) +# two_factor_grace_period :integer default(48) # class ApplicationSetting < ActiveRecord::Base @@ -42,21 +44,32 @@ class ApplicationSetting < ActiveRecord::Base attr_accessor :restricted_signup_domains_raw validates :session_expire_delay, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :home_page_url, - allow_blank: true, - url: true, - if: :home_page_url_column_exist + allow_blank: true, + url: true, + if: :home_page_url_column_exist validates :after_sign_out_path, - allow_blank: true, - url: true + allow_blank: true, + url: true validates :admin_notification_email, - allow_blank: true, - email: true + allow_blank: true, + email: true + + validates :two_factor_grace_period, + numericality: { greater_than_or_equal_to: 0 } + + validates :recaptcha_site_key, + presence: true, + if: :recaptcha_enabled + + validates :recaptcha_private_key, + presence: true, + if: :recaptcha_enabled validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? @@ -112,6 +125,8 @@ class ApplicationSetting < ActiveRecord::Base import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], + require_two_factor_authentication: false, + two_factor_grace_period: 48 ) end @@ -126,12 +141,16 @@ class ApplicationSetting < ActiveRecord::Base def restricted_signup_domains_raw=(values) self.restricted_signup_domains = [] self.restricted_signup_domains = values.split( - /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace - | # or - \s # any whitespace character - | # or - [\r\n] # any number of newline characters - /x) + /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + /x) self.restricted_signup_domains.reject! { |d| d.empty? } end + + def runners_registration_token + ensure_runners_registration_token! + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6d9cdb95295d395186ab3b6e5597b4bad8e08d79..3e67b2771c1d7322328286c2c88b23edc822509e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -135,6 +135,16 @@ module Ci predefined_variables + yaml_variables + project_variables + trigger_variables end + def merge_request + merge_requests = MergeRequest.includes(:merge_request_diff) + .where(source_branch: ref, source_project_id: commit.gl_project_id) + .reorder(iid: :asc) + + merge_requests.find do |merge_request| + merge_request.commits.any? { |ci| ci.id == commit.sha } + end + end + def project commit.project end @@ -170,7 +180,8 @@ module Ci def extract_coverage(text, regex) begin - matches = text.gsub(Regexp.new(regex)).to_a.last + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) coverage = matches.gsub(/\d+(\.\d+)?/).first if coverage.present? @@ -183,8 +194,11 @@ module Ci end def raw_trace - if File.exist?(path_to_trace) + if File.file?(path_to_trace) File.read(path_to_trace) + elsif project.ci_id && File.file?(old_path_to_trace) + # Temporary fix for build trace data integrity + File.read(old_path_to_trace) else # backward compatibility read_attribute :trace @@ -201,8 +215,8 @@ module Ci end def trace=(trace) - unless Dir.exists? dir_to_trace - FileUtils.mkdir_p dir_to_trace + unless Dir.exists?(dir_to_trace) + FileUtils.mkdir_p(dir_to_trace) end File.write(path_to_trace, trace) @@ -220,6 +234,55 @@ module Ci "#{dir_to_trace}/#{id}.log" end + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + # Should be removed in 8.4, after CI files migration has been done. + # + def old_dir_to_trace + File.join( + Settings.gitlab_ci.builds_path, + created_at.utc.strftime("%Y_%m"), + project.ci_id.to_s + ) + end + + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + # Should be removed in 8.4, after CI files migration has been done. + # + def old_path_to_trace + "#{old_dir_to_trace}/#{id}.log" + end + + ## + # Deprecated + # + # This contains a hotfix for CI build data integrity, see #4246 + # + # This method is used by `ArtifactUploader` to create a store_dir. + # Warning: Uploader uses it after AND before file has been stored. + # + # This method returns old path to artifacts only if it already exists. + # + def artifacts_path + old = File.join(created_at.utc.strftime('%Y_%m'), + project.ci_id.to_s, + id.to_s) + + old_store = File.join(ArtifactUploader.artifacts_path, old) + return old if project.ci_id && File.directory?(old_store) + + File.join( + created_at.utc.strftime('%Y_%m'), + project.id.to_s, + id.to_s + ) + end + def token project.runners_token end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 6bf596e5d3e640f0b9df5f4ba25d39df29c263f9..d2a29236942eebead2e1bb85f2087b2a283e3a06 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -218,16 +218,6 @@ module Ci update!(committed_at: DateTime.now) end - ## - # This method checks if build status should be displayed. - # - # Build status should be available only if builds are enabled - # on project level and `.gitlab-ci.yml` file is present. - # - def show_build_status? - project.builds_enabled? && ci_yaml_file - end - private def save_yaml_error(error) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f56fd3e02d472a06d839c18f6d63d189b01e896c..18a00f95b48291abe09e17d7f9cbe2869988cb19 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -95,14 +95,12 @@ module Issuable opened? || reopened? end - # Deprecated. Still exists to preserve API compatibility. def downvotes - 0 + notes.awards.where(note: "thumbsdown").count end - # Deprecated. Still exists to preserve API compatibility. def upvotes - 0 + notes.awards.where(note: "thumbsup").count end def subscribed?(user) @@ -161,6 +159,14 @@ module Issuable self.class.to_s.underscore end + # Returns a Hash of attributes to be used for Twitter card metadata + def card_attributes + { + 'Author' => author.try(:name), + 'Assignee' => assignee.try(:name) + } + end + def notes_with_associations notes.includes(:author, :project) end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index d4e3099453dce57e0a7b7a4188c4ff625fea2517..6316ee208b5a7c3b2ea60d9983f858858369feda 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -44,14 +44,14 @@ module Mentionable end def all_references(current_user = self.author, text = nil) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author) if text ext.analyze(text) else self.class.mentionable_attrs.each do |attr, options| text = send(attr) - options[:cache_key] = [self, attr] if options.delete(:cache) + options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted? ext.analyze(text, options) end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 808d80b0530fb417885ae5cf2524d77bbd0c09bc..fc6f83b918b4929240bd85acfb9906af1ea51b1a 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -37,7 +37,7 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author, load_lazy_references: true) + def participants(current_user = self.author) participants = Gitlab::ReferenceExtractor.lazily do self.class.participant_attrs.flat_map do |attr| diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 56d38fe825088f54e01662b2acca4a1757c1e165..885deaf78d2d480b0ad416a04f0d3a970ed45833 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -13,20 +13,21 @@ module TokenAuthenticatable @token_fields << token_field define_singleton_method("find_by_#{token_field}") do |token| - where(token_field => token).first if token + find_by(token_field => token) if token end define_method("ensure_#{token_field}") do current_token = read_attribute(token_field) - if current_token.blank? - write_attribute(token_field, generate_token_for(token_field)) - else - current_token - end + current_token.blank? ? write_new_token(token_field) : current_token + end + + define_method("ensure_#{token_field}!") do + send("reset_#{token_field}!") if read_attribute(token_field).blank? + read_attribute(token_field) end define_method("reset_#{token_field}!") do - write_attribute(token_field, generate_token_for(token_field)) + write_new_token(token_field) save! end end @@ -34,10 +35,15 @@ module TokenAuthenticatable private - def generate_token_for(token_field) + def write_new_token(token_field) + new_token = generate_token(token_field) + write_attribute(token_field, new_token) + end + + def generate_token(token_field) loop do token = Devise.friendly_token - break token unless self.class.unscoped.where(token_field => token).first + break token unless self.class.unscoped.find_by(token_field => token) end end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 8bfc79d88f8d1ad82027123a7f9413ba5ecb3df9..af1d7562ebe2a5312fdcf1e477a1af0812952e4f 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -16,7 +16,7 @@ class GlobalMilestone end def safe_title - @title.to_slug.to_s + @title.to_slug.normalize.to_s end def expired? diff --git a/app/models/identity.rb b/app/models/identity.rb index ad60154be710f0af8092db93d76b28ad765b5f69..8bcdc1949538f9dc67a1de39223f20fc7d68f57f 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -12,6 +12,7 @@ class Identity < ActiveRecord::Base include Sortable + include CaseSensitivity belongs_to :user validates :provider, presence: true diff --git a/app/models/issue.rb b/app/models/issue.rb index 4571d7f0ee10bed4688e06811318984dd779fa6f..80ecd15077f188864ad3719fa8bb223492cc5b58 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -86,7 +86,7 @@ class Issue < ActiveRecord::Base def referenced_merge_requests Gitlab::ReferenceExtractor.lazily do [self, *notes].flat_map do |note| - note.all_references(load_lazy_references: false).merge_requests + note.all_references.merge_requests end end.sort_by(&:iid) end diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b21aac5e43c5902e67e0ed1af87aa155ae9fcce --- /dev/null +++ b/app/models/jira_issue.rb @@ -0,0 +1,2 @@ +class JiraIssue < ExternalIssue +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f6f77a162673216a9deee9194592537c457c2b89..ac25d38eb6338be998c93c6711e54c806df77a47 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -194,9 +194,7 @@ class MergeRequest < ActiveRecord::Base similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, - "Cannot Create: This merge request already exists: #{ - similar_mrs.pluck(:title) - }" + "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}" end end end @@ -337,7 +335,7 @@ class MergeRequest < ActiveRecord::Base issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) - issues.uniq + issues.uniq(&:id) else [] end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 1c4e101cc105708c9a324dfee7b67f4f8bdf40fb..adafabbec07602a5ccbebcb0a31828ac7fd5790e 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -45,7 +45,7 @@ class Namespace < ActiveRecord::Base class << self def by_path(path) - where('lower(path) = :value', value: path.downcase).first + find_by('lower(path) = :value', value: path.downcase) end # Case insensetive search for namespace by path or name @@ -148,6 +148,6 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first + projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) end end diff --git a/app/models/note.rb b/app/models/note.rb index 8c5b5836f9a2cbcf2d2b569b2042cc36629d3c6a..3d5b663c99f0ff545c3682ac422cdcedf9b22e5a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -107,9 +107,16 @@ class Note < ActiveRecord::Base end def grouped_awards + notes = {} + awards.select(:note).distinct.map do |note| - [ note.note, where(note: note.note) ] + notes[note.note] = where(note: note.note) end + + notes["thumbsup"] ||= Note.none + notes["thumbsdown"] ||= Note.none + + notes end end @@ -339,14 +346,12 @@ class Note < ActiveRecord::Base read_attribute(:system) end - # Deprecated. Still exists to preserve API compatibility. def downvote? - false + is_award && note == "thumbsdown" end - # Deprecated. Still exists to preserve API compatibility. def upvote? - false + is_award && note == "thumbsup" end def editable? diff --git a/app/models/project.rb b/app/models/project.rb index e1f7bf971e384df359e552d56f7f96f115f5fa81..017471995ec25458e6d74233447d091113bbeb1d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,19 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + # update visibility_levet of forks + after_update :update_forks_visibility_level + def update_forks_visibility_level + return unless visibility_level < visibility_level_was + + forks.each do |forked_project| + if forked_project.visibility_level > visibility_level + forked_project.visibility_level = visibility_level + forked_project.save! + end + end + end + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags @@ -100,9 +113,12 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy - has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_from_project, through: :forked_project_link + + has_many :forked_project_links, foreign_key: "forked_from_project_id" + has_many :forks, through: :forked_project_links, source: :forked_to_project - has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed @@ -265,7 +281,7 @@ class Project < ActiveRecord::Base joins(:namespace). iwhere('namespaces.path' => namespace_path) - projects.where('projects.path' => project_path).take || + projects.find_by('projects.path' => project_path) || projects.iwhere('projects.path' => project_path).take end @@ -450,7 +466,7 @@ class Project < ActiveRecord::Base end def external_issue_tracker - @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first + @external_issues_tracker ||= external_issues_trackers.find(&:activated?) end def can_have_issues_tracker_id? @@ -496,7 +512,11 @@ class Project < ActiveRecord::Base end def ci_service - @ci_service ||= ci_services.select(&:activated?).first + @ci_service ||= ci_services.find(&:activated?) + end + + def jira_tracker? + issues_tracker.to_param == 'jira' end def avatar_type @@ -535,7 +555,9 @@ class Project < ActiveRecord::Base end def send_move_instructions(old_path_with_namespace) - NotificationService.new.project_was_moved(self, old_path_with_namespace) + # New project path needs to be committed to the DB or notification will + # retrieve stale information + run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) } end def owner @@ -547,7 +569,7 @@ class Project < ActiveRecord::Base end def project_member_by_name_or_email(name = nil, email = nil) - user = users.where('name like ? or email like ?', name, email).first + user = users.find_by('name like ? or email like ?', name, email) project_members.where(user: user) if user end @@ -722,7 +744,7 @@ class Project < ActiveRecord::Base end def project_member(user) - project_members.where(user_id: user).first + project_members.find_by(user_id: user) end def default_branch @@ -764,7 +786,7 @@ class Project < ActiveRecord::Base end def forks_count - ForkedProjectLink.where(forked_from_project_id: self.id).count + forks.count end def find_label(name) @@ -799,6 +821,10 @@ class Project < ActiveRecord::Base false end + def jira_tracker_active? + jira_tracker? && jira_service.active + end + def ci_commit(sha) ci_commits.find_by(sha: sha) end @@ -850,4 +876,13 @@ class Project < ActiveRecord::Base def build_timeout_in_minutes=(value) self.build_timeout = value.to_i * 60 end + + def open_issues_count + issues.opened.count + end + + def visibility_level_allowed?(level) + return true unless forked? + Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i) + end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 0a61ad96a0e9b6aedbfe13ccc92e68dc4642963b..aa8746beb806acef7310ce140ee734bb5ce9801d 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -27,12 +27,10 @@ class BambooService < CiService validates :build_key, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, - if: :activated? + if: ->(service) { service.activated? && service.password } validates :password, presence: true, - if: ->(service) { service.username? }, - if: :activated? + if: ->(service) { service.activated? && service.username } attr_accessor :response diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 27fc19379f1d0c49e9b018f4f7f7f4c32411646c..15c7c907f7e38dac47825390cd251f7401d0f9ea 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -58,6 +58,6 @@ class FlowdockService < Service repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", - ) + ) end end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 91ef267ad79f11eee131fe5583f03fae602f1695..202fee042e30bfe6bbea33d1b73a673c52dc53a4 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -57,6 +57,6 @@ class GemnasiumService < Service token: token, api_key: api_key, repo: project.repository.path_to_repo - ) + ) end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index d73182d40ac6235cee37728b063bf8365a4247f4..b64d97ce75dc5d2ebd90f410812b527c88a6d11d 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -18,6 +18,11 @@ # note_events :boolean default(TRUE), not null # +# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed class GitlabCiService < CiService - # this is no longer used + # We override the active accessor to always make GitLabCiService disabled + # Otherwise the GitLabCiService can be picked, but should never be since it's deprecated + def active + false + end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 35e30b1cb0b76370baa3d5c714afbde0f2269209..e216f406e1cb672187d8dadfd2f91a375b878143 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,9 +19,24 @@ # class JiraService < IssueTrackerService + include HTTParty include Gitlab::Application.routes.url_helpers - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + DEFAULT_API_VERSION = 2 + + prop_accessor :username, :password, :api_url, :jira_issue_transition_id, + :title, :description, :project_url, :issues_url, :new_issue_url + + before_validation :set_api_url, :set_jira_issue_transition_id + + before_update :reset_password + + def reset_password + # don't reset the password if a new one is provided + if api_url_changed? && !password_touched? + self.password = nil + end + end def help line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ @@ -54,4 +69,228 @@ class JiraService < IssueTrackerService def to_param 'jira' end + + def fields + super.push( + { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + { type: 'text', name: 'username', placeholder: '' }, + { type: 'password', name: 'password', placeholder: '' }, + { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } + ) + end + + def execute(push, issue = nil) + if issue.nil? + # No specific issue, that means + # we just want to test settings + test_settings + else + close_issue(push, issue) + end + end + + def create_cross_reference_note(mentioned, noteable, author) + issue_name = mentioned.id + project = self.project + noteable_name = noteable.class.name.underscore.downcase + noteable_id = if noteable.is_a?(Commit) + noteable.id + else + noteable.iid + end + + entity_url = build_entity_url(noteable_name.to_sym, noteable_id) + + data = { + user: { + name: author.name, + url: resource_url(user_path(author)), + }, + project: { + name: project.path_with_namespace, + url: resource_url(namespace_project_path(project.namespace, project)) + }, + entity: { + name: noteable_name.humanize.downcase, + url: entity_url + } + } + + add_comment(data, issue_name) + end + + def test_settings + result = JiraService.get( + jira_api_test_url, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") + true + else + Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") + false + end + rescue Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + false + end + + private + + def build_api_url_from_project_url + server = URI(project_url) + default_ports = [["http",80],["https",443]].include?([server.scheme,server.port]) + server_url = "#{server.scheme}://#{server.host}" + server_url.concat(":#{server.port}") unless default_ports + "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" + rescue + "" # looks like project URL was not valid + end + + def set_api_url + self.api_url = build_api_url_from_project_url if self.api_url.blank? + end + + def set_jira_issue_transition_id + self.jira_issue_transition_id ||= "2" + end + + def close_issue(entity, issue) + commit_id = if entity.is_a?(Commit) + entity.id + elsif entity.is_a?(MergeRequest) + entity.last_commit.id + end + commit_url = build_entity_url(:commit, commit_id) + + # Depending on the JIRA project's workflow, a comment during transition + # may or may not be allowed. Split the operation in to two calls so the + # comment always works. + transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) + end + + def transition_issue(issue) + message = { + transition: { + id: jira_issue_transition_id + } + } + send_message(close_issue_url(issue.iid), message.to_json) + end + + def add_issue_solved_comment(issue, commit_id, commit_url) + comment = { + body: "Issue solved with [#{commit_id}|#{commit_url}]." + } + + send_message(comment_url(issue.iid), comment.to_json) + end + + def add_comment(data, issue_name) + url = comment_url(issue_name) + user_name = data[:user][:name] + user_url = data[:user][:url] + entity_name = data[:entity][:name] + entity_url = data[:entity][:url] + project_name = data[:project][:name] + + message = { + body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]." + } + + unless existing_comment?(issue_name, message[:body]) + send_message(url, message.to_json) + end + end + + + def auth + require 'base64' + Base64.urlsafe_encode64("#{self.username}:#{self.password}") + end + + def send_message(url, message) + result = JiraService.post( + url, + body: message, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + message = case result.code + when 201, 200, 204 + "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." + when 401 + "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." + else + "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" + end + + Rails.logger.info(message) + message + rescue URI::InvalidURIError, Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." + end + + def existing_comment?(issue_name, new_comment) + result = JiraService.get( + comment_url(issue_name), + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + existing_comments = JSON.parse(result.body)['comments'] + + if existing_comments.present? + return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any? + end + end + + false + rescue JSON::ParserError + false + end + + def resource_url(resource) + "#{Settings.gitlab['url'].chomp("/")}#{resource}" + end + + def build_entity_url(entity_name, entity_id) + resource_url( + polymorphic_url( + [ + self.project.namespace.becomes(Namespace), + self.project, + entity_name + ], + id: entity_id, + routing_type: :path + ) + ) + end + + def close_issue_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/transitions" + end + + def comment_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/comment" + end + + def jira_api_test_url + "#{self.api_url}/myself" + end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 29d4236745a15aa46be95a2eddbd695cbf55c6cc..a63700693d778c18a99b9929c8af46116776adfb 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -27,12 +27,10 @@ class TeamcityService < CiService validates :build_type, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, - if: :activated? + if: ->(service) { service.activated? && service.password } validates :password, presence: true, - if: ->(service) { service.username? }, - if: :activated? + if: ->(service) { service.activated? && service.username } attr_accessor :response @@ -147,6 +145,6 @@ class TeamcityService < CiService '</build>', headers: { 'Content-type' => 'application/xml' }, basic_auth: auth - ) + ) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 2c25f4ce451561a71301ccdcf952b535aa32a9d6..a9bf4eb4033251fcd67261f5ebaaeca7e1e45a58 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -76,7 +76,9 @@ class Repository path: path, limit: limit, offset: offset, - follow: path.present? + # --follow doesn't play well with --skip. See: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 + follow: false } commits = Gitlab::Git::Commit.where(options) @@ -592,47 +594,54 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end - def commit_with_hooks(current_user, branch) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - was_empty = empty? - - # Create temporary ref + def with_tmp_ref(oldrev = nil) random_string = SecureRandom.hex tmp_ref = "refs/tmp/#{random_string}/head" - unless was_empty - oldrev = find_branch(branch).target + if oldrev && !Gitlab::Git.blank_ref?(oldrev) rugged.references.create(tmp_ref, oldrev) end # Make commit in tmp ref - newrev = yield(tmp_ref) + yield(tmp_ref) + ensure + rugged.references.delete(tmp_ref) rescue nil + end + + def commit_with_hooks(current_user, branch) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch + was_empty = empty? - unless newrev - raise CommitError.new('Failed to create commit') + unless was_empty + oldrev = find_branch(branch).target end - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - if was_empty - # Create branch - rugged.references.create(ref, newrev) - else - # Update head - current_head = find_branch(branch).target + with_tmp_ref(oldrev) do |tmp_ref| + # Make commit in tmp ref + newrev = yield(tmp_ref) + + unless newrev + raise CommitError.new('Failed to create commit') + end - # Make sure target branch was not changed during pre-receive hook - if current_head == oldrev - rugged.references.update(ref, newrev) + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do + if was_empty + # Create branch + rugged.references.create(ref, newrev) else - raise CommitError.new('Commit was rejected because branch received new push') + # Update head + current_head = find_branch(branch).target + + # Make sure target branch was not changed during pre-receive hook + if current_head == oldrev + rugged.references.update(ref, newrev) + else + raise CommitError.new('Commit was rejected because branch received new push') + end end end end - rescue GitHooksService::PreReceiveError - # Remove tmp ref and return error to user - rugged.references.delete(tmp_ref) - raise end private diff --git a/app/models/user.rb b/app/models/user.rb index fdd14f4571d7a136b09f5f490c17b128cb45f67b..df87f3b79bd66735c96a65eda8298d1c7fabcfe5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null @@ -220,9 +221,9 @@ class User < ActiveRecord::Base def find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if login = conditions.delete(:login) - where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first + where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase) else - where(conditions).first + find_by(conditions) end end @@ -285,7 +286,7 @@ class User < ActiveRecord::Base end def by_username_or_id(name_or_id) - where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first + find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) end def build_user(attrs = {}) diff --git a/app/services/base_service.rb b/app/services/base_service.rb index f00ec7408b683fb6f00e6c85c38ad04364bfeef3..b48ca67d4d2de1c561aedb04c307b42cdabc7903 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -39,10 +39,7 @@ class BaseService def deny_visibility_level(model, denied_visibility_level = nil) denied_visibility_level ||= model.visibility_level - level_name = 'Unknown' - Gitlab::VisibilityLevel.options.each do |name, level| - level_name = name if level == denied_visibility_level - end + level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level) model.errors.add( :visibility_level, diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index de18f3bc5567f6c2194340fc3ed26dd76c3fe9e8..f139872c728ec618300c9a354ff6dffa5a176d85 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,10 +1,10 @@ require_relative 'base_service' class CreateBranchService < BaseService - def execute(branch_name, ref) + def execute(branch_name, ref, source_project: @project) valid_branch = Gitlab::GitRefValidator.validate(branch_name) if valid_branch == false - return error('Branch name invalid') + return error('Branch name is invalid') end repository = project.repository @@ -13,7 +13,20 @@ class CreateBranchService < BaseService return error('Branch already exists') end - new_branch = repository.add_branch(current_user, branch_name, ref) + new_branch = nil + if source_project != @project + repository.with_tmp_ref do |tmp_ref| + repository.fetch_ref( + source_project.repository.path_to_repo, + "refs/heads/#{ref}", + tmp_ref + ) + + new_branch = repository.add_branch(current_user, branch_name, tmp_ref) + end + else + new_branch = repository.add_branch(current_user, branch_name, ref) + end if new_branch push_data = build_push_data(project, current_user, new_branch) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 759c334ebe96dff6b630effa700fb12d0c57376f..31b407efeb1fe60d0af3635ecafcc2553ef316d0 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -16,9 +16,23 @@ class CreateCommitBuildsService return false end - tag = Gitlab::Git.tag_ref?(origin_ref) - commit = project.ensure_ci_commit(sha) + commit = project.ci_commit(sha) + unless commit + commit = project.ci_commits.new(sha: sha) + + # Skip creating ci_commit when no gitlab-ci.yml is found + unless commit.ci_yaml_file + return false + end + + # Create a new ci_commit + commit.save! + end + + # Skip creating builds for commits that have [ci skip] unless commit.skip_ci? + # Create builds for commit + tag = Gitlab::Git.tag_ref?(origin_ref) commit.update_committed! commit.create_builds(ref, tag, user) end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 9a67b160940bfe0ca30b28107c9a1314d002da3a..0326a8823e975c20a7a02825ba1b1da94f27868f 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,8 +3,10 @@ module Files class ValidationError < StandardError; end def execute - @current_branch = params[:current_branch] + @source_project = params[:source_project] || @project + @source_branch = params[:source_branch] @target_branch = params[:target_branch] + @commit_message = params[:commit_message] @file_path = params[:file_path] @file_content = if params[:file_content_encoding] == 'base64' @@ -16,8 +18,8 @@ module Files # Validate parameters validate - # Create new branch if it different from current_branch - if @target_branch != @current_branch + # Create new branch if it different from source_branch + if different_branch? create_target_branch end @@ -26,18 +28,14 @@ module Files else error("Something went wrong. Your changes were not committed") end - rescue Repository::CommitError, GitHooksService::PreReceiveError, ValidationError => ex + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex error(ex.message) end private - def current_branch - @current_branch ||= params[:current_branch] - end - - def target_branch - @target_branch ||= params[:target_branch] + def different_branch? + @source_branch != @target_branch || @source_project != @project end def raise_error(message) @@ -52,11 +50,11 @@ module Files end unless project.empty_repo? - unless repository.branch_names.include?(@current_branch) + unless @source_project.repository.branch_names.include?(@source_branch) raise_error("You can only create or edit files when you are on a branch") end - if @current_branch != @target_branch + if different_branch? if repository.branch_names.include?(@target_branch) raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") end @@ -65,10 +63,10 @@ module Files end def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch) + result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) unless result[:status] == :success - raise_error("Something went wrong when we tried to create #{@target_branch} for you") + raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 2348920cc58c25a1900f3a40dd46e3f56dcee36f..e4cde4a2fd84e1ec3143f937a129ab4fbb13662f 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -26,7 +26,7 @@ module Files unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@current_branch, @file_path) + blob = repository.blob_at_branch(@source_branch, @file_path) if blob raise_error("Your changes could not be committed because a file with the same name already exists") diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 3d85f97b7e5a357e4c8188053e5b521f5fe156a1..a1a20e476819df6d507312edd8667da35a4a0016 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,6 +1,11 @@ module Issues class CloseService < Issues::BaseService def execute(issue, commit = nil) + if project.jira_tracker? && project.jira_service.active + project.jira_service.execute(commit, issue) + return issue + end + if project.default_issues_tracker? && issue.close event_service.close_issue(issue, current_user) create_note(issue, commit) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index b26c7513f5bd7ddd54e3f5a9244ae1815734a99a..8b3d56c2b4c1b574d496934cc71c82b35faa9d57 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -112,7 +112,7 @@ module MergeRequests merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( - merge_request, merge_request.project, @current_user, + merge_request, merge_request.project, @current_user, :source, @branch_name, presence) end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 69bdd045ddfeece023c92821cd7d7cdb3cd89853..895e089bea3c74f47cb41dd811efe5730cc09b5b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,12 +3,16 @@ module Projects def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] - if new_visibility && new_visibility.to_i != project.visibility_level - unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - deny_visibility_level(project, new_visibility) - return project + if new_visibility + if new_visibility.to_i != project.visibility_level + unless can?(current_user, :change_visibility_level, project) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(project, new_visibility) + return project + end end + + return false unless visibility_level_allowed?(new_visibility) end new_branch = params[:default_branch] @@ -23,5 +27,19 @@ module Projects end end end + + private + + def visibility_level_allowed?(level) + return true if project.visibility_level_allowed?(level) + + level_name = Gitlab::VisibilityLevel.level_name(level) + project.errors.add( + :visibility_level, + "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive" + ) + + false + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 6975b2ee55b55f2a2c713799cc37221188b91f3c..98a71cbf1ada720854f7627ce00206d709b5206d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -241,9 +241,14 @@ class SystemNoteService note_options.merge!(noteable: noteable) end - create_note(note_options) + if noteable.is_a?(ExternalIssue) + noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) + else + create_note(note_options) + end end + def self.cross_reference?(note_text) note_text.start_with?(cross_reference_note_prefix) end @@ -259,7 +264,7 @@ class SystemNoteService # # Returns Boolean def self.cross_reference_disallowed?(noteable, mentioner) - return true if noteable.is_a?(ExternalIssue) + return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 1dccc39e7e2e4d5902948453b71e19aab275866c..1b0ae6c00569b56c26c68a64c93203ccd42ae49a 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -20,16 +20,12 @@ class ArtifactUploader < CarrierWave::Uploader::Base @build, @field = build, field end - def artifacts_path - File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s) - end - def store_dir - File.join(ArtifactUploader.artifacts_path, artifacts_path) + File.join(self.class.artifacts_path, @build.artifacts_path) end def cache_dir - File.join(ArtifactUploader.artifacts_cache_path, artifacts_path) + File.join(self.class.artifacts_cache_path, @build.artifacts_path) end def file_storage? diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 6c355366948ec61bc7b62913c1ecb9f51ddf178c..214e0209bb7e31c5bf28e52a0f8c7f62c74d13b6 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -104,6 +104,18 @@ = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled + .form-group + = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :require_two_factor_authentication do + = f.check_box :require_two_factor_authentication + Require all users to setup Two-Factor authentication + .form-group + = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' + .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication .form-group = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2' .col-sm-10 @@ -144,5 +156,82 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' + %fieldset + %legend Metrics + %p + These settings require a restart to take effect. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :metrics_enabled do + = f.check_box :metrics_enabled + Enable InfluxDB Metrics + .form-group + = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' + .form-group + = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' + .help-block + The UDP port to use for connecting to InfluxDB. InfluxDB requires that + your server configuration specifies a database to store data in when + sending messages to this port, without it metrics data will not be + saved. + .form-group + = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_username, class: 'form-control' + .form-group + = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_password, class: 'form-control' + .form-group + = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_pool_size, class: 'form-control' + .help-block + The amount of InfluxDB connections to open. Connections are opened + lazily. Users using multi-threaded application servers should ensure + enough connections are available (at minimum the amount of application + server threads). + .form-group + = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_timeout, class: 'form-control' + .help-block + The amount of seconds after which an InfluxDB connection will time + out. + .form-group + = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_method_call_threshold, class: 'form-control' + .help-block + A method call is only tracked when it takes longer to complete than + the given amount of milliseconds. + + %fieldset + %legend Spam and Anti-bot Protection + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :recaptcha_enabled do + = f.check_box :recaptcha_enabled + Enable reCAPTCHA + %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts + + .form-group + = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :recaptcha_site_key, class: 'form-control' + .help-block + Generate site and private keys here: + %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + .form-group + = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :recaptcha_private_key, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8657d2c71fe83eae1a39a5b521ca922acbbdca4c..531247e9148565a0e3b475a435c5a5bd709858bb 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -79,6 +79,10 @@ GitLab API %span.pull-right = API::API::version + %p + Git + %span.pull-right + = Gitlab::Git.version %p Ruby %span.pull-right diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index 8358a14445b24c20eaa0e7edc661f42fd1187c32..741d111fb7d40c3db8e27f3127784eefc652e345 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -1,6 +1,7 @@ - page_title "Identities", @user.name, "Users" = render 'admin/users/head' += link_to 'New Identity', new_admin_user_identity_path, class: 'pull-right btn btn-new' - if @identities.present? .table-holder %table.table diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..e30bf0ef0ee23fd90f03946282c2ecf315e4b316 --- /dev/null +++ b/app/views/admin/identities/new.html.haml @@ -0,0 +1,4 @@ +- page_title "New Identity" +%h3.page-title New identity +%hr += render 'form' diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index c5fb3c95506e2ea52b4bcc2ae8b17d78525747d3..c407972cd0875dd3c4b329acd7c649216eccf0f5 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -3,7 +3,7 @@ To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. Registration token is - %code{ id: 'runners-token' } #{current_application_settings.ensure_runners_registration_token} + %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} .bs-callout.clearfix .pull-left diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index 77f78caa8d886ac22324b676429399862314e8e3..f7875e68b7e668745e10e238e7c7d1899b820a66 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -41,5 +41,3 @@ %i.fa.fa-remove.incorrect-syntax %b Error: = @error - - diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml deleted file mode 100644 index a96c0b11b6e06cb89baed0c3730f8c028883f675..0000000000000000000000000000000000000000 --- a/app/views/ci/lints/create.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $(".results").html("#{escape_javascript(render "create")}") \ No newline at end of file diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index fb9057e4882069229f557c83af711e1dbc719ab3..a144c43be477a260ccdad87422f114634285f135 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,27 +1,17 @@ %h2 Check your .gitlab-ci.yml %hr -= form_tag ci_lint_path, method: :post, remote: true do - .control-group - = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label' - .controls - = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true +.row + = form_tag ci_lint_path, method: :post do + .form-group + = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap' + .col-sm-12 + = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + .col-sm-12 + .pull-left.prepend-top-10 + = submit_tag 'Validate', class: 'btn btn-success submit-yml' - .control-group.clearfix - .controls.pull-left.prepend-top-10 - = submit_tag "Validate", class: 'btn btn-success submit-yml' - - -%p.text-center.loading - %i.fa.fa-refresh.fa-spin - -.results.prepend-top-20 - -:javascript - $(".loading").hide(); - $('form').bind('ajax:beforeSend', function() { - $(".loading").show(); - }); - $('form').bind('ajax:complete', function() { - $(".loading").hide(); - }); +.row.prepend-top-20 + .col-sm-12 + .results + = render partial: 'create' if defined?(@status) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 2e77afb7525923d48e085aaaf47d61cc39c1e781..f4a3e3162bf5da1eeaecc7d4b6b56f290a94f427 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,13 +1,20 @@ = content_for :flash_message do = render 'shared/project_limit' +.top-area + %ul.left-top-menu + = nav_link(page: [dashboard_projects_path, root_path]) do + = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + Your Projects + = nav_link(page: starred_dashboard_projects_path) do + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + Starred Projects + = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do + = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do + Explore Projects -%ul.center-top-menu - = nav_link(page: [dashboard_projects_path, root_path]) do - = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects - = nav_link(page: starred_dashboard_projects_path) do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - Starred Projects - = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do - = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do - Explore Projects + .projects-search-form + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false + - if current_user.can_create_project? + = link_to new_project_path, class: 'btn btn-green' do + %i.fa.fa-plus + New Project diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 44b7efe523277e32a59cef41fb0631b75068e2fe..4316c358dcb81c2aa0f87e490b95499d529b8156 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,18 +1,18 @@ - page_title @milestone.title, "Milestones" - header_title "Milestones", dashboard_milestones_path -.issuable-details - .page-title - .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open +.detail-page-header + .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? + Closed + - else + Open + %span.identifier Milestone #{@milestone.title} - .gray-content-block.middle-block - %h2.issue-title - = markdown escape_once(@milestone.title), pipeline: :single_line +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line - if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index 81a5909e2d2ac5f01430f000bf0512306320cedd..cea9ffcc748ad866e87d924b1785da19b94f10e4 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1,11 +1,3 @@ .projects-list-holder - .projects-search-form - .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - - if current_user.can_create_project? - %span.input-group-btn - = link_to new_project_path, class: 'btn btn-green' do - %i.fa.fa-plus - New Project = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb deleted file mode 100644 index 79d6c761d8fce48f4bf9e757392c13cff3ecf16d..0000000000000000000000000000000000000000 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<p>Hello <%= @resource.email %>!</p> - -<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p> - -<p>Click the link below to unlock your account:</p> - -<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p> diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..52b327e20c5f71dec4a876fa2585ebfc921066c1 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -0,0 +1,10 @@ +%p +Hello #{@resource.name}! + +%p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in + = time_ago_in_words(Devise.unlock_in.from_now) + or you may click the link below to unlock now. + +%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 9dc6aeffd5947b903780be359101ab3ae8aa2cb5..cb93ff2465e055a77f1ee1405d02b129ed199f0b 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -6,17 +6,21 @@ .login-heading %h3 Create an account .login-body + - user = params[:user].present? ? params[:user] : {} = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| .devise-errors = devise_error_messages! %div - = f.text_field :name, class: "form-control top", placeholder: "Name", required: true + = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true %div - = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true + = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true %div - = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true + = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true + %div + - if current_application_settings.recaptcha_enabled + = recaptcha_tags %div = f.submit "Sign up", class: "btn-create btn" diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb deleted file mode 100644 index f9277d1673fe35c0ab7a80339cb20c1159871823..0000000000000000000000000000000000000000 --- a/app/views/devise/unlocks/new.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<h2>Resend unlock instructions</h2> - -<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> - <%= devise_error_messages! %> - - <div><%= f.label :email %><br /> - <%= f.email_field :email %></div> - - <div><%= f.submit "Resend unlock instructions" %></div> -<% end %> - -<%= render partial: "devise/shared/links" %> diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..49c087c0646e42112a6266225eb0ecf0aa225039 --- /dev/null +++ b/app/views/devise/unlocks/new.html.haml @@ -0,0 +1,14 @@ +.login-box + .login-heading + %h3 Resend unlock email + .login-body + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + .devise-errors + = devise_error_messages! + .clearfix.append-bottom-20 + = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' + .clearfix + = f.submit 'Resend unlock instructions', class: 'btn btn-success' + +.clearfix.prepend-top-20 + = render 'devise/shared/sign_in_link' diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 76bdd68fd764b3f8a7f9f8d0f7683adb0fc06e35..b9a958fbe7be505ab4a7aa705e91beef3dc518d4 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -6,7 +6,7 @@ - else = render 'explore/head' -.gray-content-block.clearfix +.gray-content-block.clearfix.second-block = render 'filter' = render 'projects', projects: @projects = paginate @projects, theme: "gitlab" diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index e30c3633223c026e86400fa0c47a7283e63d0aa9..95d46e331f8a1d4dc41aeee78142645c349440cc 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -7,7 +7,7 @@ = render 'explore/head' .explore-trending-block - .gray-content-block + .gray-content-block.second-block .pull-right = render 'explore/projects/dropdown' .oneline diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 1412b19acde9b2fa25ea5f5596a8e965d4d106ec..fa0b718e48b7b37c6fc02b2dd347b9875a5d8264 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -7,7 +7,7 @@ = render 'explore/head' .explore-trending-block - .gray-content-block + .gray-content-block.second-block .pull-right = render 'explore/projects/dropdown' .oneline diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 11d69977ef9e03b87fd2c5bb113cd131651fb1d7..bbafc08435af438ca1f99bd3948208d73138910f 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,5 +1,5 @@ -.panel.panel-default.projects-list-holder - .panel-heading.clearfix +.projects-list-holder + .projects-search-form .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if can? current_user, :create_projects, @group diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 8daac5859601fcff465dbe568f449a2db0b0fcf9..1dea77c2e96be9c8211f3157448d268d56e9d362 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -31,7 +31,7 @@ .col-sm-10 .checkbox = f.check_box :public - %span.descr Make this group public (even if there is no any public project inside this group) + %span.descr Make this group public (even if there are no public projects inside this group) .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 350e216fcc61f6e2a61c12fb197a9fb924534090..d063b257b5e6df7aae5f9d79fdbdfc165718f228 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,24 +1,24 @@ - page_title @milestone.title, "Milestones" = render "header_title" -.issuable-details - .page-title - .issue-box{ class: "issue-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open +.detail-page-header + .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? + Closed + - else + Open + %span.identifier Milestone #{@milestone.title} - .pull-right - - if can?(current_user, :admin_milestones, @group) - - if @milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" - - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + .pull-right + - if can?(current_user, :admin_milestones, @group) + - if @milestone.active? + = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" + - else + = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" - .gray-content-block.middle-block - %h2.issue-title - = markdown escape_once(@milestone.title), pipeline: :single_line +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line - if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index dc8e81323a63daf43f7e527162142042b3a37111..c2c7c581b3eff57c5ad3d522f86bc77544a72709 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,37 +5,47 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") -.dashboard - .header-with-avatar.clearfix - = image_tag group_icon(@group), class: "avatar group-avatar s90" - %h3 - = @group.name - .username - @#{@group.path} - - if @group.description.present? - .description - = markdown(@group.description, pipeline: :description) - %hr - - = render 'shared/show_aside' - - - if can?(current_user, :read_group, @group) - .row - %section.activities.col-md-7 - .hidden-xs - - if current_user - = render "events/event_last_push", event: @last_push - .pull-right - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do - %i.fa.fa-rss - - = render 'shared/event_filter' - %hr - - .content_list - = spinner - %aside.side.col-md-5 - = render "projects", projects: @projects - - else - %p - This group does not have public projects +.cover-block + .avatar-holder + = link_to group_icon(@group), target: '_blank' do + = image_tag group_icon(@group), class: "avatar group-avatar s90" + .cover-title + = @group.name + + .cover-desc.username + @#{@group.path} + + - if @group.description.present? + .cover-desc.description + = markdown(@group.description, pipeline: :description) + +- if can?(current_user, :read_group, @group) + %ul.center-top-menu.no-top + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @projects.present? + %li + = link_to "#projects", 'data-toggle' => 'tab' do + Projects + + .tab-content + .tab-pane.active#activity + .gray-content-block.activity-filter-block + - if current_user + = render "events/event_last_push", event: @last_push + .pull-right + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + + .content_list + = spinner + + .tab-pane#projects + = render "projects", projects: @projects + +- else + %p + This group does not have public projects diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 7e801b5332d9cc848a98e92f51c8e84db0bed059..e8e331dd10902ca7af6cbba70b36b2eab26aa642 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -219,11 +219,3 @@ %td.shortcut .key r %td Reply (quoting selected text) - - -:javascript - $('.js-more-help-button').click(function (e) { - $(this).remove()l - $('.hidden-shortcut').show(); - e.preventDefault(); - }); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 74174a72f5a8c39d972e339c02d61f528063e8bf..2e0bd2007a3bf16b49c19741ebd638b570568466 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,10 +1,26 @@ -- page_title "GitLab" -%head +%head{prefix: "og: http://ogp.me/ns#"} %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} - %meta{content: "GitLab Community Edition", name: "description"} %meta{name: 'referrer', content: 'origin-when-cross-origin'} + %meta{name: "description", content: page_description} + + -# Open Graph - http://ogp.me/ + %meta{property: 'og:type', content: "object"} + %meta{property: 'og:site_name', content: "GitLab"} + %meta{property: 'og:title', content: page_title} + %meta{property: 'og:description', content: page_description} + %meta{property: 'og:image', content: page_image} + %meta{property: 'og:url', content: request.base_url + request.fullpath} + + -# Twitter Card - https://dev.twitter.com/cards/types/summary + %meta{property: 'twitter:card', content: "summary"} + %meta{property: 'twitter:title', content: page_title} + %meta{property: 'twitter:description', content: page_description} + %meta{property: 'twitter:image', content: page_image} + = page_card_meta_tags + + - page_title "GitLab" %title= page_title = favicon_link_tag 'favicon.ico' diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 27112c6745a657c3072e00385d230861fecff1cc..00cb4aa24cc28b60b4e1c2c98efc7eff2050a1b6 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,4 +1,2 @@ -%div - "#{link_to @note.author_name, user_url(@note.author)} wrote:" %div = markdown(@note.note, pipeline: :email) diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index 11166dc6d9985e4b626289a4e28b09c817533c78..13a18269d11b57c0dc2fd6ee3ca257ce59cd4306 100644 --- a/app/views/profiles/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -12,6 +12,6 @@ comment = val.match(/^\S+ \S+ (.+)\n?$/); if( comment && comment.length > 1 && title.val() == '' ){ - $('#key_title').val( comment[1] ); + $('#key_title').val( comment[1] ).change(); } }); diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 92dc58c10d71f643e733a15773f2c02e39141395..1a5b6efce3558e457a129203bff3fad0589f28a2 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -38,3 +38,4 @@ = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true .form-actions = submit_tag 'Submit', class: 'btn btn-success' + = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 2fd3d9e1be454bc889bc57c9419f5c9b9583b502..640612ca433d0ac29e6bb6381d914c001a6925cf 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -2,3 +2,7 @@ = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} + + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index c1669ac046b4207ae92914d078b0871e8e8bec0b..e92115b9b987ba29111ba28c8f6be4991c7816c6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -27,7 +27,7 @@ = icon('rss') .project-repo-buttons - .split-one + .split-one.count-buttons = render 'projects/buttons/star' = render 'projects/buttons/fork' @@ -38,3 +38,6 @@ = render 'projects/buttons/dropdown' = render 'projects/buttons/notifications' + +:coffeescript + new Star() \ No newline at end of file diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index b1df8d19938326eab5ec0622846a3d401257a5d3..cdac50f7a8d3a5c7c14d57659c38b114ae8262e9 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -2,7 +2,7 @@ = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - - if blob_viewable?(@blob) + - if blob_text_viewable?(@blob) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' @@ -14,13 +14,8 @@ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if blob_editable?(@blob) +- if current_user .btn-group{ role: "group" } - = edit_blob_link(@project, @ref, @path) - %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace - %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete -- elsif !on_top_of_branch? - .btn-group{ role: "group" } - %button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on a branch.", data: {container: 'body'}} Edit - %button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on a branch.", data: {container: 'body'}} Replace - %button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on a branch.", data: {container: 'body'}} Delete + = edit_blob_link + = replace_blob_link + = delete_blob_link diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index fc6c9f5fd094d8f57c5252324e734852554f943c..084608bbba38c7a5f64fb923ec7ce9e868b88a86 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -17,5 +17,9 @@ = submit_tag "Create directory", class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help + :javascript new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index ecc90a30e78869103b0930f68e3d2d6cef7ba62d..676924dc6ca2d366ba98717f882c20a4b837682d 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -20,6 +20,11 @@ = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help + + :javascript disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); new BlobFileDropzone($('.js-upload-blob-form'), '#{method}'); diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a47fe7ede8059d19e7839114d90d855970b3bdb3..09fa148b129b03ec047f78748462478afe4784df 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -20,7 +20,7 @@ = hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, cancel_path: @after_edit_path + = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) :javascript blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 3f8d11ed8c85614ceec49bd1df31d3a63c77942c..6988039b6c7233e30de71f7fc300fa7a949b18d0 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,7 +6,7 @@ %div#tree-holder.tree-holder = render 'blob', blob: @blob -- if blob_editable?(@blob) +- if can_edit_blob?(@blob) = render 'projects/blob/remove' - title = "Replace #{@blob.name}" diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 31943a2407a275fd00717251fdfb2f287678298d..c659af6338c477dacc83c28417567c7064f20c0b 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -9,11 +9,12 @@ New Branch %hr -= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' + = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 @@ -26,7 +27,4 @@ :javascript var availableRefs = #{@project.repository.ref_names.to_json}; - $("#ref").autocomplete({ - source: availableRefs, - minLength: 1 - }); + new NewBranchForm($('.js-create-branch-form'), availableRefs) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 20a5b6a66e7038d573028777ec9c4c3a457ad9fb..5b7ecce86ab2b9041019e526650a50e45eb124a0 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -7,6 +7,10 @@ %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) from = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) + - merge_request = @build.merge_request + - if merge_request + via + = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) #up-build-trace - if @commit.matrix_for_ref?(@build.ref) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index b277b765b6b526a0c123e01823c0e9cdd9012f5a..1f639fecc308decd4fb7bff135ec9c6be8ca2108 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -18,10 +18,11 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet + - if can?(current_user, :push_code, @project) %li.divider %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), title: 'New file' do + = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') New file %li @@ -32,3 +33,20 @@ = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') New tag + - elsif current_user && current_user.already_forked?(@project) + %li.divider + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do + = icon('file fw') + New file + - elsif can?(current_user, :fork_project, @project) + %li.divider + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + New file diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 2d3abf09051852a7bab10bf71923ce8726e86614..133531887a202cfabb18762643723d48d437c55f 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -4,10 +4,15 @@ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do = icon('code-fork fw') Fork + %div.count-with-arrow + %span.arrow %span.count = @project.forks_count - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do = icon('code-fork fw') + Fork + %div.count-with-arrow + %span.arrow %span.count = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 41a3ec6d90fafbed9a389e5cd59486ad678c4837..21ba426aaa1e779a139aa29bc25ecc32be41c49f 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,19 +1,21 @@ - if current_user = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do - = icon('star fw') - %span.count + - if current_user.starred?(@project) + = icon('star fw') + %span.starred Unstar + - else + = icon('star-o fw') + %span Star + %div.count-with-arrow + %span.arrow + %span.count.star-count = @project.star_count - :javascript - $('.project-home-panel .toggle-star').on('ajax:success', function (e, data, status, xhr) { - $(this).replaceWith(data.html); - }) - .on('ajax:error', function (e, xhr, status, error) { - new Flash('Star toggle failed. Try again later.', 'alert'); - }); - - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do = icon('star fw') + Star + %div.count-with-arrow + %span.arrow %span.count = @project.star_count diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 634924db247f4405c2be374e8c6dd0a0b60a7d60..ddb77fd796b13f03c74cc7e9aea68e8e0bc46e37 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -20,8 +20,8 @@ %p %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace", data: { clipboard_text: @commit.id } - = clipboard_button + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + = clipboard_button(clipboard_text: @commit.id) .commit-info-row %span.light Authored by %strong @@ -40,7 +40,7 @@ - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @ci_commit && @ci_commit.show_build_status? +- if @ci_commit .pull-right = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do = ci_status_icon(@ci_commit) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 45a00e4d259d82a8d6076939e22ea74250d87906..74a05df24d3db44b5365de2cc6f562325156cd12 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -19,11 +19,11 @@ - if defined?(commit_sha) && commit_sha %td - = link_to commit_status.short_sha, namespace_project_commit_path(@project.namespace, @project, commit_status.sha), class: "monospace" - + = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace" + %td - if commit_status.ref - = link_to commit_status.ref, namespace_project_commits_path(@project.namespace, @project, commit_status.ref) + = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) - else .light none @@ -66,7 +66,7 @@ %td .pull-right - - if current_user && can?(current_user, :download_build_artifacts, @project) && commit_status.download_url + - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url = link_to commit_status.download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, commit_status.project) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1303b27c4f34b3b9a46613d22575ef07075d1fe0..28b82dd31f3eb549d1eb7111f0c4d501b09efb5f 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -17,7 +17,7 @@ %a.text-expander.js-toggle-button ... .pull-right - - if ci_commit && ci_commit.show_build_status? + - if ci_commit = render_ci_status(ci_commit) = clipboard_button(clipboard_text: commit.id) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 327e7d9245a625f916a538e0d3805a546c99d3db..517f6aef7c5a89e356407058e781455467cb8640 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -24,7 +24,7 @@ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - - if blob_viewable?(blob) + - if blob_text_viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments @@ -32,14 +32,15 @@ - if editable_diff?(diff_file) = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, - after: ' ', from_merge_request_id: @merge_request.id) + from_merge_request_id: @merge_request.id) + = view_file_btn(diff_commit.id, diff_file, project) .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if blob_viewable?(blob) + - if blob_text_viewable?(blob) - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index f0b0a11c04a30de76383dd410d22bac157c3d580..8a2c027a45517d6ad288122d5b416c840f9ac006 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -43,4 +43,3 @@ %i.fa.fa-spinner.fa-spin Forking repository %p Please wait a moment, this page will automatically refresh when ready. - diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index 3c491c1a8b8a6a40077e8346ad6311f1a404b4ba..de415ae51a41d1979f408fc8102716dfd2c98f06 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,2 @@ -.issue-closed-by-widget - = icon('check') +.issue-closed-by-widget.gray-content-block.second-block.white This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted. diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 405bae1bbb9148eb1479897f6aa3a5b3485bb9e7..dc434cf38c4ab006432903456cfd5dace77a4865 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,12 +1,9 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - if @issue.closed? - = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' - else - = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' - -.gray-content-block.second-block.oneline-block - = render 'votes/votes_block', votable: @issue + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue' #notes = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index fe856ac991e17d07d1188914e71dd381e5be632d..254968e4f678f7850a25b3c74d33a77dd8b90531 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -15,9 +15,10 @@ %span.merge-request-info %strong = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" - in - - project = merge_request.target_project - = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) + - unless @issue.project.id == merge_request.target_project.id + in + - project = merge_request.target_project + = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) %span.merge-request-status.prepend-left-10 - if merge_request.merged? MERGED diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index cc2cf8c871698721344641f0baba101f9f180da7..f931a0d3b92bcdfc6f2bfb8830f6f23ebfe43ada 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,61 +1,64 @@ -- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_description @issue.description +- page_card_attributes @issue.card_attributes + = render "header_title" .issue + .detail-page-header + .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed + .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open + %span.identifier + Issue ##{@issue.iid} + %span.creator + · + opened by #{link_to_member(@project, @issue.author, size: 24)} + · + = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') + - if @issue.updated_at != @issue.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') + + .pull-right + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do + = icon('plus') + New Issue + - if can?(current_user, :update_issue, @issue) + = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue' + = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue' + + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do + = icon('pencil-square-o') + Edit + .issue-details.issuable-details - .issuable-title - .issue-box{ class: issue_box_class(@issue) } - - if @issue.closed? - Closed - - else - Open - %span.issuable-id Issue ##{@issue.iid} - %span.creator - · - opened by #{link_to_member(@project, @issue.author, size: 24)} - · - = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') - - if @issue.updated_at != @issue.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') + .detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@issue.title), pipeline: :single_line + %div + - if @issue.description.present? + .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} + .wiki + = preserve do + = markdown(@issue.description, cache_key: [@issue, "description"]) + %textarea.hidden.js-task-list-field + = @issue.description - .pull-right - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do - = icon('plus') - New Issue - - if can?(current_user, :update_issue, @issue) - - if @issue.closed? - = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen' - - else - = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue' + .merge-requests + = render 'merge_requests' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do - = icon('pencil-square-o') - Edit + .gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @issue + + - if @closed_by_merge_requests.present? + = render 'projects/issues/closed_by_box' .row %section.col-md-9 - .gray-content-block - %h2.issue-title - = markdown escape_once(@issue.title), pipeline: :single_line - %div - - if @issue.description.present? - .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} - .wiki - = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) - %textarea.hidden.js-task-list-field - = @issue.description - - .merge-requests - = render 'merge_requests' - - - if @closed_by_merge_requests.present? - = render 'projects/issues/closed_by_box' - .issue-discussion + .issuable-discussion = render 'projects/issues/discussion' %aside.col-md-3 diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 7a7428d35ccc18bf7caf8e160db438fccf606b42..bff3c3b283ddf33a3fee9104980120eb62136021 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,11 +1,8 @@ - content_for :note_actions do - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" - -.gray-content-block.second-block.oneline-block - = render 'votes/votes_block', votable: @merge_request + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 105c731c7e14a2327c9086107d20046ca7446160..a051729dc3202221a75da4595df695a9f4eaead2 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -17,7 +17,7 @@ - if merge_request.open? && merge_request.broken? %li - = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: {container: 'body'} do + = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do = icon('exclamation-triangle') - if merge_request.assignee diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 4172d5a4e88b4a06a99442c89e0ba90dcade23f6..a14943b15d3f1532ac6b0e94fb3162817988a16c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,15 +23,15 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size - %li.diffs-tab.active - = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @diffs.size - if @ci_commit %li.builds-tab.active = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds %span.badge= @statuses.size + %li.diffs-tab.active + = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + Changes + %span.badge= @diffs.size .tab-content #commits.commits.tab-pane diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 04f8fd7442207094ef5596b8b6796f654e31f14e..ba7c2c01e93a7ac6760633a325672ab9db40b1e8 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,85 +1,91 @@ -- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_description @merge_request.description +- page_card_attributes @merge_request.card_attributes + = render "header_title" - if params[:view] == 'parallel' - fluid_layout true .merge-request{'data-url' => merge_request_path(@merge_request)} - .merge-request-details.issuable-details - = render "projects/merge_requests/show/mr_title" - .row - %section.col-md-9 - = render "projects/merge_requests/show/mr_box" - .append-bottom-default.mr-source-target.prepend-top-default - - if @merge_request.open? - .pull-right - - if @merge_request.source_branch_exists? - = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do - = icon('cloud-download fw') - Check out branch + = render "projects/merge_requests/show/mr_title" - %span.dropdown - %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + .merge-request-details.issuable-details + = render "projects/merge_requests/show/mr_box" + .append-bottom-default.mr-source-target.prepend-top-default + - if @merge_request.open? + .pull-right + - if @merge_request.source_branch_exists? + = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do + = icon('cloud-download fw') + Check out branch - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + %span.dropdown + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch - - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) - .light.prepend-top-default - You can also accept this merge request manually using the - = succeed '.' do - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/widget/show.html.haml" - - if @commits.present? - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do - Commits - %span.badge= @commits.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @merge_request.diffs.size - - if @ci_commit - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do - Builds - %span.badge= @statuses.size + - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) + .light.prepend-top-default + You can also accept this merge request manually using the + = succeed '.' do + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - .tab-content - #notes.notes.tab-pane.voting_notes - = render "projects/merge_requests/discussion" - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX + - if @commits.present? + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do + Discussion + %span.badge= @merge_request.mr_and_commit_notes.user.count + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do + Commits + %span.badge= @commits.size + - if @ci_commit + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diffs.size - .mr-loading-status - = spinner + .tab-content + #notes.notes.tab-pane.voting_notes + .gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @merge_request - %aside.col-md-3 - = render 'shared/issuable/sidebar', issuable: @merge_request + .row + %section.col-md-9 + .issuable-discussion + = render "projects/merge_requests/discussion" + %aside.col-md-3 + = render 'shared/issuable/sidebar', issuable: @merge_request + = render 'shared/show_aside' - = render 'shared/show_aside' + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX + .mr-loading-status + = spinner :javascript var merge_request; diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 98f0357ce4ea4699632e01c2a6e469a68aa1eadd..877cc3d744b9bf6855d52efd241a78691e0a5139 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -8,8 +8,8 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-1') + %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch} @@ -25,8 +25,8 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-3') + %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve git checkout #{h @merge_request.target_branch} @@ -38,8 +38,8 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-4') + %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} - unless @merge_request.can_be_merged_by?(current_user) 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 9bfe202589ec0a7d31fe18918ef2f3481baf244b..0f81e5e891424ff42f77c0011efc7e87cc79288c 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,5 +1,5 @@ -.gray-content-block.middle-block - %h2.issue-title +.detail-page-description.gray-content-block.second-block + %h2.title = markdown escape_once(@merge_request.title), pipeline: :single_line %div diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index d65c3b16618827f5891fdaa466fec1f7f57bf096..fc6fb2a0d42f56b243870a06a83d27d722a7d6ee 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,7 +1,8 @@ -.issuable-title - .issue-box{ class: issue_box_class(@merge_request) } +.detail-page-header + .status-box{ class: status_box_class(@merge_request) } = @merge_request.state_human_name - %span.issuable-id Merge Request ##{@merge_request.iid} + %span.identifier + Merge Request ##{@merge_request.iid} %span.creator · opened by #{link_to_member(@project, @merge_request.author, size: 24)} @@ -16,9 +17,9 @@ .issue-btn-group.pull-right - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do %i.fa.fa-pencil-square-o Edit - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request' diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 6f52c963a530a6d6a2bcbe4f49af6a546777da06..d1d602eecdcdc08986ed25822956416b2c470904 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -8,19 +8,15 @@ #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} %div - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') - = succeed '.' do - The changes were merged into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. The source branch has been removed. - elsif @merge_request.can_remove_source_branch?(current_user) .remove_source_branch_widget %p - = succeed '.' do - The changes were merged into - = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do - = @merge_request.target_branch + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. You can remove the source branch now. = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do %i.fa.fa-times diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index c6bc4ca5bebfaff1793a92c9ac44e20dda208561..d9a1730a8bc96f25c81d021d255ad33c36334aad 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -7,13 +7,13 @@ .accept-action - if @ci_commit && @ci_commit.active? %span.btn-group - = link_to "#", class: "btn btn-create merge_when_build_succeeds" do + = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do Merge When Build Succeeds - %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do %span.caret %span.sr-only Select Merge Moment - %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } %li = link_to "#", class: "merge_when_build_succeeds" do = icon('check fw') @@ -23,7 +23,7 @@ = icon('warning fw') Merge Immediately - else - = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do + = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do Accept Merge Request - if @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox @@ -42,21 +42,19 @@ = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" :javascript - $('.accept_merge_request').on('click', function() { - $(this).html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); - }); - $('.accept-mr-form').on('ajax:send', function() { $(".accept-mr-form :input").disable(); }); - $('a.accept_merge_request').on('click', function(e) { - e.preventDefault(); - $(this).closest("form").submit(); + $('.accept_merge_request').on('click', function() { + $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); }); - $('a.merge_when_build_succeeds').on('click', function(e) { - e.preventDefault(); + $('.merge_when_build_succeeds').on('click', function() { $("#merge_when_build_succeeds").val("1"); + }); + + $('.js-merge-dropdown a').on('click', function(e) { + e.preventDefault(); $(this).closest("form").submit(); }); diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 7ecee4403373311e5304895fb717c7afbb887c7b..1670ea8741a3a07065f93f38a89e4dc13b771812 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,44 +1,46 @@ -- page_title @milestone.title, "Milestones" +- page_title @milestone.title, "Milestones" +- page_description @milestone.description + = render "header_title" -.issuable-details - .page-title - .issue-box{ class: issue_box_class(@milestone) } - - if @milestone.closed? - Closed - - elsif @milestone.expired? - Expired - - else - Open +.detail-page-header + .status-box{ class: status_box_class(@milestone) } + - if @milestone.closed? + Closed + - elsif @milestone.expired? + Expired + - else + Open + %span.identifier Milestone ##{@milestone.iid} - - if @milestone.expires_at - %span.creator - · - = @milestone.expires_at - .pull-right - - if can?(current_user, :admin_milestone, @project) - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - - - if @milestone.active? - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - - else - = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" - - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do - %i.fa.fa-trash-o - Delete - - .gray-content-block.middle-block - %h2.issue-title - = markdown escape_once(@milestone.title), pipeline: :single_line - %div - - if @milestone.description.present? - .description - .wiki - = preserve do - = markdown @milestone.description + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at + .pull-right + - if can?(current_user, :admin_milestone, @project) + - if @milestone.active? + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" + - else + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do + %i.fa.fa-trash-o + Delete + + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do + %i.fa.fa-pencil-square-o + Edit + +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line + %div + - if @milestone.description.present? + .description + .wiki + = preserve do + = markdown @milestone.description - if @milestone.issues.any? && @milestone.can_be_closed? .alert.alert-success.prepend-top-default diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 88e711ab5341d848e3e76270338819d32e970fab..acb6dc52a8e204f7fecfc7d2f70eeabe844041dc 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -13,6 +13,6 @@ .error-alert .note-form-actions.clearfix - = f.submit 'Add Comment', class: "btn btn-create comment-btn btn-grouped js-comment-button" + = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" = yield(:note_actions) %a.btn.btn-cancel.js-close-discussion-note-form Cancel diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9c7a5584da9a1122a09121c940dc26c29da46461..7466a098e24d0ba1e735d307ffc41c17c7a458b2 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -71,7 +71,7 @@ = render default_project_view - if current_user - - access = user_max_access_in_project(current_user, @project) + - access = user_max_access_in_project(current_user.id, @project) - if access .prepend-top-20.project-footer .gray-content-block.footer-block.center diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 1bc90edd8f0342c35352cd162328c55a14e3f00f..1927883513afbf2a2e0b96e8ea1973a97da658fe 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -29,7 +29,7 @@ - if tree.readme = render "projects/tree/readme", readme: tree.readme -- if allowed_tree_edit? +- if can_edit_tree? = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post = render 'projects/blob/new_dir' diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index cefe33e581f97d79fd9b318161946f291a50cbe6..3343288ad2b1da800a7ce50eca1176ea2c19ce9d 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -11,26 +11,65 @@ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' - - if allowed_tree_edit? + + - if current_user %li - %span.dropdown - %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} + - if !on_top_of_branch? + %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} = icon('plus') - %ul.dropdown-menu - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do - = icon('pencil fw') - Create file - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do - = icon('file fw') - Upload file - %li.divider - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do - = icon('folder fw') - New directory - - elsif !on_top_of_branch? - %li - %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}} - = icon('plus') + - else + %span.dropdown + %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + - if can_edit_tree? + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do + = icon('pencil fw') + New file + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = icon('file fw') + Upload file + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = icon('folder fw') + New directory + - elsif can?(current_user, :fork_project, @project) + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('pencil fw') + New file + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + Upload file + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('folder fw') + New directory + + %li.divider + %li + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + New branch + %li + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + New tag diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index ce8ddff955690ea4895580cdb23945e75fd8bd7d..45d700781f3e3d14547268ef9444810112a8e32e 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -6,7 +6,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(issue.description)) + = search_md_sanitize(markdown(issue.description, { project: issue.project })) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index edb5778f4240740ef66bbbf4560a3b0074b6e3b7..687a59c270f9f35d5b940aa740ac60b18f453839 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,10 +1,27 @@ - project = project || @project -.git-clone-holder.input-group - .input-group-addon.git-protocols - .input-group-btn - = ssh_clone_button(project) - .input-group-btn - = http_clone_button(project) + +.git-clone-holder + .btn-group.clone-options + %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} + %span + = default_clone_protocol.upcase + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown + %li + %a#ssh-selector{href: @project.ssh_url_to_repo} + SSH + %li + %a#http-selector{href: @project.http_url_to_repo} + HTTPS + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn = clipboard_button(clipboard_target: '#project_clone') + +:javascript + $('ul.clone-options-dropdown a').on('click',function(e){ + e.preventDefault(); + var $this = $(this); + $('a.clone-dropdown-btn span').text($this.text()); + $('#project_clone').val($this.attr('href')); + }); diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 111219f2064c69b8952b38f5e9935138570e5a20..0c8ac48bb58f8d3ced26fbbf7d54f7bb58f61d83 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -1,16 +1,22 @@ = render 'shared/commit_message_container', placeholder: placeholder -- unless @project.empty_repo? - .form-group.branch - = label_tag 'new_branch', 'Target branch', class: 'control-label' - .col-sm-10 - = text_field_tag 'new_branch', @new_branch || tree_edit_branch, required: true, class: "form-control js-new-branch" +- if @project.empty_repo? + = hidden_field_tag 'target_branch', @ref +- else + - if can?(current_user, :push_code, @project) + .form-group.branch + = label_tag 'target_branch', 'Target branch', class: 'control-label' + .col-sm-10 + = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch" - .js-create-merge-request-container - .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" - Start a <strong>new merge request</strong> with these changes + .js-create-merge-request-container + .checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + Start a <strong>new merge request</strong> with these changes + - else + = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch + = hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index ac6c248ccf1df9f6e6b67dff7ce1ecb157f60b16..be06738eac900b5b1b07c87c2c0438187c145d1c 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -29,14 +29,14 @@ = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" .issues-other-filters - .filter-item.inline - = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true) - .filter-item.inline = users_select_tag(:author_id, selected: params[:author_id], placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + .filter-item.inline + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true) + .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, class: 'select2 trigger-submit', include_blank: true, diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 91ccd1ef6601f24986abf27f18affe227bcb7316..90dc00624818e2631f55a905b85a38be4fb74303 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -19,7 +19,7 @@ - else Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a <strong>Work In Progress</strong> merge request from being merged before it's ready. -.form-group.issuable-description +.form-group.detail-page-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0019f739b89d8d453a82c9791ffcff317112354e..79c5cc7f40aa27a1e7bde511f2aff57499a7f09e 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,13 +1,5 @@ .issuable-sidebar.issuable-affix = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| - .block - .title - Cross-project reference - .cross-project-reference - %span#cross-project-reference - = cross_project_reference(@project, issuable) - = clipboard_button(clipboard_target: 'span#cross-project-reference') - .block.assignee .title %label @@ -62,6 +54,14 @@ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } + .block + .title + Cross-project reference + .cross-project-reference + %span#cross-project-reference + = cross_project_reference(@project, issuable) + = clipboard_button(clipboard_target: 'span#cross-project-reference') + = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 669e6119fb65a54a8d29b52acf6d8b1881b5105d..aa5acee9c14c1e495826bdb6a9c0a58637691347 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,25 +1,25 @@ -.issuable-details - .page-title - .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} - = visibility_level_icon(@snippet.visibility_level, fw: false) - = visibility_level_label(@snippet.visibility_level) +.detail-page-header + .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} + = visibility_level_icon(@snippet.visibility_level, fw: false) + = visibility_level_label(@snippet.visibility_level) + %span.identifier Snippet ##{@snippet.id} - %span.creator - · created by #{link_to_member(@project, @snippet.author, size: 24)} - · - = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') + %span.creator + · created by #{link_to_member(@project, @snippet.author, size: 24)} + · + = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + - if @snippet.updated_at != @snippet.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') - .pull-right - - if @snippet.project_id? - = render "projects/snippets/actions" - - else - = render "snippets/actions" + .pull-right + - if @snippet.project_id? + = render "projects/snippets/actions" + - else + = render "snippets/actions" - .gray-content-block.middle-block - %h2.issue-title - = markdown escape_once(@snippet.title), pipeline: :single_line +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@snippet.title), pipeline: :single_line diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index a0a6e2d9810b0f071f61e95b33de961638dcefd0..0bca8177e14e43f7fd892579a99612adbe19df58 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,6 @@ -- page_title @user.name -- header_title @user.name, user_path(@user) +- page_title @user.name +- page_description @user.bio +- header_title @user.name, user_path(@user) = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") @@ -73,7 +74,7 @@ .user-calendar-activities -%ul.center-top-menu.no-top.no-bottom.bottom-border +%ul.center-top-menu.no-top.no-bottom.bottom-border.wide %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 6071f1484c621f05b316a8b23555d66f1835e276..ce0a0113403328f7c4d75b540fab4d746c03714f 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,33 +1,46 @@ .awards.votes-block - - votable.notes.awards.grouped_awards.each do |emoji, notes| + - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} - .icon{"data-emoji" => "#{emoji}"} - = image_tag url_to_emoji(emoji), height: "20px", width: "20px" + = emoji_icon(emoji) .counter = notes.count - if current_user - .dropdown.awards-controls + .awards-controls %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"} = icon('smile-o') - %ul.dropdown-menu.awards-menu - - emoji_list.each do |emoji| - %li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px" + .emoji-menu + .emoji-menu-content + = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" + - AwardEmoji.emoji_by_category.each do |category, emojis| + %h5= AwardEmoji::CATEGORIES[category] + %ul + - emojis.each do |emoji| + %li + = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) - if current_user :coffeescript post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" noteable_type = "#{votable.class.name.underscore}" noteable_id = "#{votable.id}" - aliases = #{AwardEmoji::ALIASES.to_json} - window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id, aliases) + aliases = #{AwardEmoji.aliases.to_json} - $(".awards-menu li").click (e)-> - emoji = $(this).data("emoji") + window.awards_handler = new AwardsHandler( + post_emoji_url, + noteable_type, + noteable_id, + aliases + ) + + $(".awards").on "click", ".emoji-menu-content li", (e) -> + emoji = $(this).find(".emoji-icon").data("emoji") awards_handler.addAward(emoji) - $(".awards").on "click", ".award", (e)-> + $(".awards").on "click", ".award", (e) -> emoji = $(this).find(".icon").data("emoji") awards_handler.addAward(emoji) $(".award").tooltip() + + $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false}) diff --git a/config/database.yml.env b/config/database.yml.env new file mode 100644 index 0000000000000000000000000000000000000000..b2ff23cb5abda9327b0e39d8bb5564b14cd092fe --- /dev/null +++ b/config/database.yml.env @@ -0,0 +1,9 @@ +<%= ENV['RAILS_ENV'] %>: + adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %> + encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %> + database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %> + pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %> + username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %> + password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %> + host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %> + port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %> diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index db378118f8550d9e0589a4f70717f5e3d38b3f1d..2d9f730c1831e6de76b8d297089b484449588553 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -4,8 +4,8 @@ # ########################### NOTE ##################################### # This file should not receive new settings. All configuration options # -# that do not require an application restart are being moved to # -# ApplicationSetting model! # +# * are being moved to ApplicationSetting model! # +# If a setting requires an application restart say so in that screen. # # If you change this file in a Merge Request, please also create # # a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # ######################################################################## @@ -144,6 +144,15 @@ production: &base # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon + ## Auxiliary jobs + # Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc. + # Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job + cron_jobs: + # Flag stuck CI builds as failed + stuck_ci_builds_worker: + cron: "0 0 * * *" + + # # 2. GitLab CI settings # ========================== @@ -287,6 +296,15 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: + # See omniauth-cas3 for more configuration details + # - { name: 'cas3', + # label: 'cas3', + # args: { + # url: 'https://sso.example.com', + # disable_ssl_verification: false, + # login_url: '/cas/login', + # service_validate_url: '/cas/p3/serviceValidate', + # logout_url: '/cas/logout'} } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', @@ -324,6 +342,10 @@ production: &base # application_name: 'YOUR_APP_NAME', # application_password: 'YOUR_APP_PASSWORD' } } + # SSO maximum session duration in seconds. Defaults to CAS default of 8 hours. + # cas3: + # session_duration: 28800 + # Shared file storage settings shared: # path: /mnt/gitlab # Default: shared diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 63d8ae174365bc7f6613a109a42671745b573191..4fbd84ee8901067b0de09c27cc6d025e1f63d352 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,6 +126,11 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['providers'] ||= [] +Settings.omniauth['cas3'] ||= Settingslogic.new({}) +Settings.omniauth.cas3['session_duration'] ||= 8.hours +Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) +Settings.omniauth.session_tickets['cas3'] = 'ticket' + Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) @@ -140,16 +145,16 @@ Settings.gitlab['default_projects_limit'] ||= 10 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? -Settings.gitlab['host'] ||= 'localhost' +Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? -Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" -Settings.gitlab['email_display_name'] ||= "GitLab" -Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}" +Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" +Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' +Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}" Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url) Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' @@ -164,7 +169,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled']. Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z]*-\d*))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 @@ -224,6 +229,15 @@ Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url']) +# +# Cron Jobs +# +Settings['cron_jobs'] ||= Settingslogic.new({}) +Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' + + # # GitLab Shell # diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index bfb8656df552da088bf7848deaa77540945bead2..df28d30d750946bf32d3d4e5ae078bc7e3919ab0 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -31,11 +31,11 @@ if File.exists?(aws_file) if Rails.env.test? Fog.mock! connection = ::Fog::Storage.new( - aws_access_key_id: AWS_CONFIG['access_key_id'], - aws_secret_access_key: AWS_CONFIG['secret_access_key'], - provider: 'AWS', - region: AWS_CONFIG['region'] - ) + aws_access_key_id: AWS_CONFIG['access_key_id'], + aws_secret_access_key: AWS_CONFIG['secret_access_key'], + provider: 'AWS', + region: AWS_CONFIG['region'] + ) connection.directories.create(key: AWS_CONFIG['bucket']) end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5fb43a86e13d385f5831108859636a28bce56384..d82cfb3ec0c787022f6b61fcab2f5104f0fee8f0 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -121,14 +121,14 @@ Devise.setup do |config| config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [ :email ] + config.unlock_keys = [ :email ] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. - config.unlock_strategy = :time + config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. @@ -241,6 +241,16 @@ Devise.setup do |config| # An Array from the configuration will be expanded. provider_arguments.concat provider['args'] when Hash + # Add procs for handling SLO + if provider['name'] == 'cas3' + provider['args'][:on_single_sign_out] = lambda do |request| + ticket = request.params[:session_index] + raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) + Gitlab::OAuth::Session.destroy(:cas3, ticket) + true + end + end + # A Hash from the configuration will be passed as is. provider_arguments << provider['args'].symbolize_keys end diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb new file mode 100644 index 0000000000000000000000000000000000000000..2e4908192a13ba6d03a3f3fec455fd2a9661cec7 --- /dev/null +++ b/config/initializers/metrics.rb @@ -0,0 +1,64 @@ +if Gitlab::Metrics.enabled? + require 'influxdb' + require 'socket' + require 'connection_pool' + require 'method_source' + + # These are manually require'd so the classes are registered properly with + # ActiveSupport. + require 'gitlab/metrics/subscribers/action_view' + require 'gitlab/metrics/subscribers/active_record' + + Gitlab::Application.configure do |config| + config.middleware.use(Gitlab::Metrics::RackMiddleware) + end + + Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add Gitlab::Metrics::SidekiqMiddleware + end + end + + # This instruments all methods residing in app/models that (appear to) use any + # of the ActiveRecord methods. This has to take place _after_ initializing as + # for some unknown reason calling eager_load! earlier breaks Devise. + Gitlab::Application.config.after_initialize do + Rails.application.eager_load! + + models = Rails.root.join('app', 'models').to_s + + regex = Regexp.union( + ActiveRecord::Querying.public_instance_methods(false).map(&:to_s) + ) + + Gitlab::Metrics::Instrumentation. + instrument_class_hierarchy(ActiveRecord::Base) do |klass, method| + # Instrumenting the ApplicationSetting class can lead to an infinite + # loop. Since the data is cached any way we don't really need to + # instrument it. + if klass == ApplicationSetting + false + else + loc = method.source_location + + loc && loc[0].start_with?(models) && method.source =~ regex + end + end + end + + Gitlab::Metrics::Instrumentation.configure do |config| + config.instrument_instance_methods(Gitlab::Shell) + + config.instrument_methods(Gitlab::Git) + + Gitlab::Git.constants.each do |name| + const = Gitlab::Git.const_get(name) + + config.instrument_methods(const) if const.is_a?(Module) + end + end + + GC::Profiler.enable + + Gitlab::Metrics::Sampler.new.start +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 2e3a71912ef6b26600838c8a35c4e142c648f614..dcf6ce74d96bbd1dc546a6d0934e5a611d7b8cd7 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -18,11 +18,12 @@ Sidekiq.configure_server do |config| chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] end - # Sidekiq-cron: load recurring jobs from schedule.yml - schedule_file = 'config/schedule.yml' - if File.exists?(schedule_file) - Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file) - end + # Sidekiq-cron: load recurring jobs from gitlab.yml + # UGLY Hack to get nested hash from settingslogic + cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json) + # UGLY hack: Settingslogic doesn't allow 'class' key + cron_jobs.each { |k,v| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') } + Sidekiq::Cron::Job.load_from_hash! cron_jobs # Database pool should be at least `sidekiq_concurrency` + 2 # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md diff --git a/config/routes.rb b/config/routes.rb index e2d4fcb65a8446acd6678229803abfb662021df9..3e7d9f78710fcc31b1ee2efbf5e25bc742278f8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -188,7 +188,7 @@ Rails.application.routes.draw do namespace :admin do resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do resources :keys, only: [:show, :destroy] - resources :identities, only: [:index, :edit, :update, :destroy] + resources :identities, except: [:show] delete 'stop_impersonation' => 'impersonation#destroy', on: :collection @@ -297,6 +297,7 @@ Rails.application.routes.draw do resource :two_factor_auth, only: [:new, :create, :destroy] do member do post :codes + patch :skip end end end @@ -441,7 +442,7 @@ Rails.application.routes.draw do scope do post( - '/create_dir/*id', + '/create_dir/*id', to: 'tree#create_dir', constraints: { id: /.+/ }, as: 'create_dir' diff --git a/config/schedule.rb b/config/schedule.rb deleted file mode 100644 index 8122f7cc69c7e8165da16e9f2f4e2c935190d98b..0000000000000000000000000000000000000000 --- a/config/schedule.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Use this file to easily define all of your cron jobs. -# -# If you make changes to this file, please create also an issue on -# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary -# because the omnibus packages manage cron jobs using Chef instead of Whenever. -every 1.hour do - rake "ci:schedule_builds" -end diff --git a/config/schedule.yml b/config/schedule.yml deleted file mode 100644 index 993a95fef565b87d049e4d6e15e3e0ed7c33ee2e..0000000000000000000000000000000000000000 --- a/config/schedule.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Here is a list of jobs that are scheduled to run periodically. -# We use a UNIX cron notation to specify execution schedule. -# -# Please read here for more information: -# https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job - -stuck_ci_builds_worker: - cron: "0 0 * * *" - class: "StuckCiBuildsWorker" - queue: "default" diff --git a/db/migrate/20151012173029_set_jira_service_api_url.rb b/db/migrate/20151012173029_set_jira_service_api_url.rb new file mode 100644 index 0000000000000000000000000000000000000000..2af99e0db0b3f3624b7ef7866609a247357addc3 --- /dev/null +++ b/db/migrate/20151012173029_set_jira_service_api_url.rb @@ -0,0 +1,50 @@ +class SetJiraServiceApiUrl < ActiveRecord::Migration + # This migration can be performed online without errors, but some Jira API calls may be missed + # when doing so because api_url is not yet available. + + def build_api_url_from_project_url(project_url, api_version) + # this is the exact logic previously used to build the Jira API URL from project_url + server = URI(project_url) + default_ports = [80, 443].include?(server.port) + server_url = "#{server.scheme}://#{server.host}" + server_url.concat(":#{server.port}") unless default_ports + "#{server_url}/rest/api/#{api_version}" + end + + def get_api_version_from_api_url(api_url) + match = /\/rest\/api\/(?<api_version>\w+)$/.match(api_url) + match && match['api_version'] + end + + def change + reversible do |dir| + select_all("SELECT id, properties FROM services WHERE services.type IN ('JiraService')").each do |jira_service| + id = jira_service["id"] + properties = JSON.parse(jira_service["properties"]) + properties_was = properties.clone + + dir.up do + # remove api_version and set api_url + if properties['api_version'].present? && properties['project_url'].present? + begin + properties['api_url'] ||= build_api_url_from_project_url(properties['project_url'], properties['api_version']) + rescue + # looks like project_url was not a valid URL. Do nothing. + end + end + properties.delete('api_version') if properties.include?('api_version') + end + + dir.down do + # remove api_url and set api_version (default to '2') + properties['api_version'] ||= get_api_version_from_api_url(properties['api_url']) || '2' + properties.delete('api_url') if properties.include?('api_url') + end + + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end + end +end diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb index a84be7db3f15ba2d87f54bdd52560df34021c2af..c5542cb864da8ca5198f8d76dd8ff6fe61d991b5 100644 --- a/db/migrate/20151203162134_add_build_events_to_services.rb +++ b/db/migrate/20151203162134_add_build_events_to_services.rb @@ -1,5 +1,5 @@ class AddBuildEventsToServices < ActiveRecord::Migration - def up + def change add_column :services, :build_events, :boolean, default: false, null: false add_column :web_hooks, :build_events, :boolean, default: false, null: false end diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb index 825ba1973ff85a67a7d88fee7a416a94221d2f3e..d7e196e6763d442f4e650584f0378cb770d75c57 100644 --- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb +++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb @@ -10,4 +10,7 @@ class MigrateCiWebHooks < ActiveRecord::Migration 'JOIN projects ON ci_projects.gitlab_id = projects.id' ) end + + def down + end end diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ea66ba65dfa6df8896b219dcedbb9aac460b9cd --- /dev/null +++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb @@ -0,0 +1,5 @@ +class AddUnlockTokenToUser < ActiveRecord::Migration + def change + add_column :users, :unlock_token, :string + end +end diff --git a/db/migrate/20151210125928_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb index 8a65abab636bd9895cb93e913414275517326c08..8c167f64a2b59444c94a5b8a1f914a252e44aec1 100644 --- a/db/migrate/20151210125928_add_ci_to_project.rb +++ b/db/migrate/20151210125928_add_ci_to_project.rb @@ -1,5 +1,5 @@ class AddCiToProject < ActiveRecord::Migration - def up + def change add_column :projects, :ci_id, :integer add_column :projects, :builds_enabled, :boolean, default: true, null: false add_column :projects, :shared_runners_enabled, :boolean, default: true, null: false diff --git a/db/migrate/20151210125929_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb index 5d1cf54357673f65f3c0d084bc39d8127453e2db..84273591fa2ede4f606e2ab9cae2418330e00cf1 100644 --- a/db/migrate/20151210125929_add_project_id_to_ci.rb +++ b/db/migrate/20151210125929_add_project_id_to_ci.rb @@ -1,5 +1,5 @@ class AddProjectIdToCi < ActiveRecord::Migration - def up + def change add_column :ci_builds, :gl_project_id, :integer add_column :ci_runner_projects, :gl_project_id, :integer add_column :ci_triggers, :gl_project_id, :integer diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb index 7dfe05174ee06be52419f4b63e0359c114183577..c32c7feb1931443f5e28b1ee0d56eae5cbe71674 100644 --- a/db/migrate/20151210125930_migrate_ci_to_project.rb +++ b/db/migrate/20151210125930_migrate_ci_to_project.rb @@ -14,6 +14,10 @@ class MigrateCiToProject < ActiveRecord::Migration migrate_ci_service end + def down + # We can't reverse the data + end + def migrate_project_id_for_table(table) subquery = "SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = #{table}.project_id" execute("UPDATE #{table} SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL") @@ -26,7 +30,8 @@ class MigrateCiToProject < ActiveRecord::Migration def migrate_project_column(column, new_column = nil) new_column ||= column - subquery = "SELECT ci_projects.#{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id" + subquery = "SELECT ci_projects.#{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id " \ + 'ORDER BY ci_projects.updated_at DESC LIMIT 1' execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE (#{subquery}) IS NOT NULL") end diff --git a/db/migrate/20151210125931_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb index 9fedb5d612ce3c801487b761de65207336a0af4f..5e129c9303d9b0bece0d8fc36100682f8a8a969f 100644 --- a/db/migrate/20151210125931_add_index_to_ci_tables.rb +++ b/db/migrate/20151210125931_add_index_to_ci_tables.rb @@ -1,5 +1,5 @@ class AddIndexToCiTables < ActiveRecord::Migration - def up + def change add_index :ci_builds, :gl_project_id add_index :ci_runner_projects, :gl_project_id add_index :ci_triggers, :gl_project_id diff --git a/db/migrate/20151210125932_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb index 0b007430b0c17168447bc51f80059779f501764c..c520c2ed56f67e2f08d91fa29174d12bcb413ece 100644 --- a/db/migrate/20151210125932_drop_null_for_ci_tables.rb +++ b/db/migrate/20151210125932_drop_null_for_ci_tables.rb @@ -1,5 +1,5 @@ class DropNullForCiTables < ActiveRecord::Migration - def up + def change remove_index :ci_variables, :project_id remove_index :ci_runner_projects, :project_id change_column_null :ci_triggers, :project_id, true diff --git a/db/migrate/20151218154042_add_tfa_to_application_settings.rb b/db/migrate/20151218154042_add_tfa_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd95db775c5e5e548b8d567223fd8c9dc2607de0 --- /dev/null +++ b/db/migrate/20151218154042_add_tfa_to_application_settings.rb @@ -0,0 +1,8 @@ +class AddTfaToApplicationSettings < ActiveRecord::Migration + def change + change_table :application_settings do |t| + t.boolean :require_two_factor_authentication, default: false + t.integer :two_factor_grace_period, default: 48 + end + end +end diff --git a/db/migrate/20151221234414_add_tfa_additional_fields.rb b/db/migrate/20151221234414_add_tfa_additional_fields.rb new file mode 100644 index 0000000000000000000000000000000000000000..c16df47932f6e902d58e5758bc5ff86047cfdfc8 --- /dev/null +++ b/db/migrate/20151221234414_add_tfa_additional_fields.rb @@ -0,0 +1,7 @@ +class AddTfaAdditionalFields < ActiveRecord::Migration + def change + change_table :users do |t| + t.datetime :otp_grace_period_started_at, null: true + end + end +end diff --git a/db/migrate/20151224123230_rename_emojis.rb b/db/migrate/20151224123230_rename_emojis.rb new file mode 100644 index 0000000000000000000000000000000000000000..62d921dfdcce3e26b7efd0afd2a3dd192bddeead --- /dev/null +++ b/db/migrate/20151224123230_rename_emojis.rb @@ -0,0 +1,15 @@ +# Migration type: online without errors (works on previous version and new one) +class RenameEmojis < ActiveRecord::Migration + def up + # Renames aliases to main names + execute("UPDATE notes SET note ='thumbsup' WHERE is_award = true AND note = '+1'") + execute("UPDATE notes SET note ='thumbsdown' WHERE is_award = true AND note = '-1'") + execute("UPDATE notes SET note ='poop' WHERE is_award = true AND note = 'shit'") + end + + def down + execute("UPDATE notes SET note ='+1' WHERE is_award = true AND note = 'thumbsup'") + execute("UPDATE notes SET note ='-1' WHERE is_award = true AND note = 'thumbsdown'") + execute("UPDATE notes SET note ='shit' WHERE is_award = true AND note = 'poop'") + end +end diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..3012bd52cfdd96c09ed4bd9cdd13996e41e65d1b --- /dev/null +++ b/db/migrate/20151228150906_influxdb_settings.rb @@ -0,0 +1,18 @@ +class InfluxdbSettings < ActiveRecord::Migration + def change + add_column :application_settings, :metrics_enabled, :boolean, default: false + + add_column :application_settings, :metrics_host, :string, + default: 'localhost' + + add_column :application_settings, :metrics_database, :string, + default: 'gitlab' + + add_column :application_settings, :metrics_username, :string + add_column :application_settings, :metrics_password, :string + add_column :application_settings, :metrics_pool_size, :integer, default: 16 + add_column :application_settings, :metrics_timeout, :integer, default: 10 + add_column :application_settings, :metrics_method_call_threshold, + :integer, default: 10 + end +end diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..259fd0248d2914c52f95a8353668458b8d0d9ce7 --- /dev/null +++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb @@ -0,0 +1,9 @@ +class AddRecaptchaToApplicationSettings < ActiveRecord::Migration + def change + change_table :application_settings do |t| + t.boolean :recaptcha_enabled, default: false + t.string :recaptcha_site_key + t.string :recaptcha_private_key + end + end +end diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..ae0499f936d6fcfd719c3c231e2ecabf975c4f26 --- /dev/null +++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb @@ -0,0 +1,5 @@ +class InfluxdbUdpPortSetting < ActiveRecord::Migration + def change + add_column :application_settings, :metrics_port, :integer, default: 8089 + end +end diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..f0e1ee1e7a79a0d3720160796fc1ffa389039fc8 --- /dev/null +++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb @@ -0,0 +1,5 @@ +class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration + def change + remove_column :application_settings, :metrics_database + end +end diff --git a/db/schema.rb b/db/schema.rb index 0167e30ff8b90658ddd9a0796d3c5720a3be376a..df7f72d5ad4a7591dde54f7a9cdc18fc9a8b0871 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151210125932) do +ActiveRecord::Schema.define(version: 20151229112614) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -33,23 +33,36 @@ ActiveRecord::Schema.define(version: 20151210125932) do t.datetime "created_at" t.datetime "updated_at" t.string "home_page_url" - t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true + t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" - t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false + t.boolean "version_check_enabled", default: true + t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" - t.boolean "user_oauth_applications", default: true + t.boolean "user_oauth_applications", default: true t.string "after_sign_out_path" - t.integer "session_expire_delay", default: 10080, null: false + t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" t.text "help_page_text" t.string "admin_notification_email" - t.boolean "shared_runners_enabled", default: true, null: false - t.integer "max_artifacts_size", default: 100, null: false + t.boolean "shared_runners_enabled", default: true, null: false + t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.boolean "require_two_factor_authentication", default: false + t.integer "two_factor_grace_period", default: 48 + t.boolean "metrics_enabled", default: false + t.string "metrics_host", default: "localhost" + t.string "metrics_username" + t.string "metrics_password" + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 + t.boolean "recaptcha_enabled", default: false + t.string "recaptcha_site_key" + t.string "recaptcha_private_key" + t.integer "metrics_port", default: 8089 end create_table "audit_events", force: :cascade do |t| @@ -783,12 +796,12 @@ ActiveRecord::Schema.define(version: 20151210125932) do add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -796,22 +809,22 @@ ActiveRecord::Schema.define(version: 20151210125932) do t.datetime "created_at" t.datetime "updated_at" t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false + t.integer "theme_id", default: 1, null: false t.string "bio" - t.integer "failed_attempts", default: 0 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" t.datetime "last_credential_check_at" @@ -820,23 +833,25 @@ ActiveRecord::Schema.define(version: 20151210125932) do t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", default: "", null: false t.string "notification_email" - t.boolean "hide_no_password", default: false - t.boolean "password_automatically_set", default: false + t.boolean "hide_no_password", default: false + t.boolean "password_automatically_set", default: false t.string "location" t.string "encrypted_otp_secret" t.string "encrypted_otp_secret_iv" t.string "encrypted_otp_secret_salt" - t.boolean "otp_required_for_login", default: false, null: false + t.boolean "otp_required_for_login", default: false, null: false t.text "otp_backup_codes" - t.string "public_email", default: "", null: false - t.integer "dashboard", default: 0 - t.integer "project_view", default: 0 + t.string "public_email", default: "", null: false + t.integer "dashboard", default: 0 + t.integer "project_view", default: 0 t.integer "consumed_timestep" - t.integer "layout", default: 0 - t.boolean "hide_project_limit", default: false + t.integer "layout", default: 0 + t.boolean "hide_project_limit", default: false + t.string "unlock_token" + t.datetime "otp_grace_period_started_at" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/README.md b/doc/README.md index a309809421078e82e1aaed7ce5cfd8eb04d5e71b..8a297f8267f11385dc4bcaaa4c25a39554b6fa4d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -7,6 +7,7 @@ - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). - [Markdown](markdown/markdown.md) GitLab's advanced formatting system. +- [Migrating from SVN](migration/README.md) Convert a SVN repository to Git and GitLab - [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. @@ -27,17 +28,18 @@ - [Using SSH keys](ci/ssh_keys/README.md) - [User permissions](ci/permissions/README.md) - [API](ci/api/README.md) +- [Triggering builds through the API](ci/triggers/README.md) ### CI Languages -+ [Testing PHP](ci/languages/php.md) +- [Testing PHP](ci/languages/php.md) ### CI Services -+ [Using MySQL](ci/services/mysql.md) -+ [Using PostgreSQL](ci/services/postgres.md) -+ [Using Redis](ci/services/redis.md) -+ [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services) +- [Using MySQL](ci/services/mysql.md) +- [Using PostgreSQL](ci/services/postgres.md) +- [Using Redis](ci/services/redis.md) +- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services) ### CI Examples @@ -54,6 +56,7 @@ - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](logs/logs.md) Log system. +- [Environmental Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running - [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. diff --git a/doc/administration/enviroment_variables.md b/doc/administration/enviroment_variables.md new file mode 100644 index 0000000000000000000000000000000000000000..7d8f9d29eef2fdca00f1fc03d37afaf46578a49c --- /dev/null +++ b/doc/administration/enviroment_variables.md @@ -0,0 +1,48 @@ +# Environment Variables + +## Introduction + +Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package. + +But if you prefer to use environment variables we allow that too. + +## Supported environment variables + +Variable | Type | Explanation +-------- | ---- | ----------- +GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation +GITLAB_HOST | url | hostname of the GitLab server includes http or https +RAILS_ENV | production / development / staging / test | Rails environment +DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5 +GITLAB_EMAIL_FROM | email | Email address used in the "From" field in mails sent by GitLab +GITLAB_EMAIL_DISPLAY_NAME | string | Name used in the "From" field in mails sent by GitLab +GITLAB_EMAIL_REPLY_TO | email | Email address used in the "Reply-To" field in mails sent by GitLab + +## Complete database variables + +As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set: + +- adapter +- database +- username +- password +- host +- port + +To do so please `cp config/database.yml.env config/database.yml` and use the following variables: + +Variable | Default +--- | --- +GITLAB_DATABASE_ADAPTER | postgresql +GITLAB_DATABASE_ENCODING | unicode +GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV'] +GITLAB_DATABASE_POOL | 10 +GITLAB_DATABASE_USERNAME | root +GITLAB_DATABASE_PASSWORD | +GITLAB_DATABASE_HOST | localhost +GITLAB_DATABASE_PORT | 5432 + +## Other variables + +We welcome merge requests to make more settings configurable via variables. +Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}". diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 366a1f8abecdcf7d692dbf442b20453c29a22d67..8bc0a67067a1a22685a7cbf8bac444b73d0c9201 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -4,8 +4,7 @@ Get all merge requests for this project. The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). -The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and -`downvotes` are deprecated and always return `0`. +The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. ``` GET /projects/:id/merge_requests @@ -58,7 +57,7 @@ Parameters: ## Get single MR -Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`. +Shows information about a single merge request. ``` GET /projects/:id/merge_request/:merge_request_id @@ -141,8 +140,6 @@ Parameters: ## Get single MR changes Shows information about the merge request including its files and changes. -With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and -always return `0`. ``` GET /projects/:id/merge_request/:merge_request_id/changes @@ -213,9 +210,7 @@ Parameters: ## Create MR -Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and ` -downvotes` are deprecated and always return `0`. - +Creates a new merge request. ``` POST /projects/:id/merge_requests ``` @@ -266,8 +261,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes` -are deprecated and always return `0`. +Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -318,8 +312,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Accept MR -Merge changes submitted with MR using this API. With GitLab 8.2 the return -fields `upvotes` and `downvotes` are deprecated and always return `0`. +Merge changes submitted with MR using this API. If merge success you get `200 OK`. diff --git a/doc/api/notes.md b/doc/api/notes.md index 4d7ef288df8ae45e2d0ab82bf4528b1342c3ae04..d4d63e825abe801e09dd17e282a22f6110a41790 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -6,8 +6,7 @@ Notes are comments on snippets, issues or merge requests. ### List project issue notes -Gets a list of all notes for a single issue. With GitLab 8.2 the return fields -`upvote` and `downvote` are deprecated and always return `false`. +Gets a list of all notes for a single issue. ``` GET /projects/:id/issues/:issue_id/notes diff --git a/doc/api/projects.md b/doc/api/projects.md index 1a52440062758f3eff1115b29cdc9d3eb8f8468d..0ca81ffd49ee1b39636a9d9045d2e6d8022523b6 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -58,6 +58,7 @@ Parameters: "path": "diaspora-client", "path_with_namespace": "diaspora/diaspora-client", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, @@ -100,6 +101,7 @@ Parameters: "path": "puppet", "path_with_namespace": "brightbox/puppet", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, @@ -116,6 +118,16 @@ Parameters: "path": "brightbox", "updated_at": "2013-09-30T13:46:02Z" }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, "archived": false, "avatar_url": null } @@ -137,6 +149,21 @@ Parameters: - `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` - `search` (optional) - Return list of authorized projects according to a search criteria +### List starred projects + +Get a list of projects which are starred by the authenticated user. + +``` +GET /projects/starred +``` + +Parameters: + +- `archived` (optional) - if passed, limit by archived status +- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at` +- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc` +- `search` (optional) - Return list of authorized projects according to a search criteria + ### List ALL projects Get a list of all GitLab projects (admin only). @@ -189,6 +216,7 @@ Parameters: "path": "diaspora-project-site", "path_with_namespace": "diaspora/diaspora-project-site", "issues_enabled": true, + "open_issues_count": 1, "merge_requests_enabled": true, "builds_enabled": true, "wiki_enabled": true, diff --git a/doc/api/users.md b/doc/api/users.md index 7ba2db248ff0568cb2f24f524ac479beb000385d..66d2fd52526fda22713743ae6a16e0b7fdc84b49 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -90,7 +90,17 @@ GET /users You can search for users by email or username with: `/users?search=John` -Also see `def search query` in `app/models/user.rb`. +In addition, you can lookup users by username: + +``` +GET /users?username=:username +``` + +For example: + +``` +GET /users?username=jack_smith +``` ## Single user diff --git a/doc/ci/README.md b/doc/ci/README.md index 5d9d7a81db349d197491dfafc9675841cddd67cb..a1f5513d88e5079e5c919ce13da55ae3ae29b0d8 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -2,28 +2,30 @@ ### User documentation -+ [Quick Start](quick_start/README.md) -+ [Configuring project (.gitlab-ci.yml)](yaml/README.md) -+ [Configuring runner](runners/README.md) -+ [Configuring deployment](deployment/README.md) -+ [Using Docker Images](docker/using_docker_images.md) -+ [Using Docker Build](docker/using_docker_build.md) -+ [Using Variables](variables/README.md) -+ [Using SSH keys](ssh_keys/README.md) +* [Quick Start](quick_start/README.md) +* [Configuring project (.gitlab-ci.yml)](yaml/README.md) +* [Configuring runner](runners/README.md) +* [Configuring deployment](deployment/README.md) +* [Using Docker Images](docker/using_docker_images.md) +* [Using Docker Build](docker/using_docker_build.md) +* [Using Variables](variables/README.md) +* [Using SSH keys](ssh_keys/README.md) +* [Triggering builds through the API](triggers/README.md) ### Languages -+ [Testing PHP](languages/php.md) +* [Testing PHP](languages/php.md) ### Services -+ [Using MySQL](services/mysql.md) -+ [Using PostgreSQL](services/postgres.md) -+ [Using Redis](services/redis.md) -+ [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services) +* [Using MySQL](services/mysql.md) +* [Using PostgreSQL](services/postgres.md) +* [Using Redis](services/redis.md) +* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services) ### Examples ++ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) + [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) + [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) + [Test Clojure applications](examples/test-clojure-application.md) @@ -31,5 +33,5 @@ ### Administrator documentation -+ [User permissions](permissions/README.md) -+ [API](api/README.md) +* [User permissions](permissions/README.md) +* [API](api/README.md) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 8d4bd44053e06eb14c0f1160d865443320351a48..31458d61674d1c45e82907443d65638abfe23f42 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -1,11 +1,11 @@ # Using Docker Images -GitLab CI in conjuction with [GitLab Runner](../runners/README.md) can use +GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use [Docker Engine](https://www.docker.com/) to test and build any application. Docker is an open-source project that allows you to use predefined images to run applications in independent "containers" that are run within a single Linux -instance. [Docker Hub][hub] has a rich database of prebuilt images that can be +instance. [Docker Hub][hub] has a rich database of pre-built images that can be used to test and build your applications. Docker, when used with GitLab CI, runs each build in a separate and isolated @@ -136,6 +136,24 @@ Look for the `[runners.docker]` section: The image and services defined this way will be added to all builds run by that runner. +## Define an image from a private Docker registry + +Starting with GitLab Runner 0.6.0, you are able to define images located to +private registries that could also require authentication. + +All you have to do is be explicit on the image definition in `.gitlab-ci.yml`. + +```yaml +image: my.registry.tld:5000/namepace/image:tag +``` + +In the example above, GitLab Runner will look at `my.registry.tld:5000` for the +image `namespace/image:tag`. + +If the repository is private you need to authenticate your GitLab Runner in the +registry. Learn how to do that on +[GitLab Runner's documentation][runner-priv-reg]. + ## Accessing the services Let's say that you need a Wordpress instance to test some API integration with @@ -258,3 +276,4 @@ creation. [tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/ [postgres-hub]: https://registry.hub.docker.com/u/library/postgres/ [mysql-hub]: https://registry.hub.docker.com/u/library/mysql/ +[runner-priv-reg]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9f7c1bfe6a036a22f19e5b00ffbe82b45ca5449b --- /dev/null +++ b/doc/ci/triggers/README.md @@ -0,0 +1,172 @@ +# Triggering Builds through the API + +_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_ + +Triggers can be used to force a rebuild of a specific branch, tag or commit, +with an API call. + +## Add a trigger + +You can add a new trigger by going to your project's **Settings > Triggers**. +The **Add trigger** button will create a new token which you can then use to +trigger a rebuild of this particular project. + +Every new trigger you create, gets assigned a different token which you can +then use inside your scripts or `.gitlab-ci.yml`. You also have a nice +overview of the time the triggers were last used. + + + +## Revoke a trigger + +You can revoke a trigger any time by going at your project's +**Settings > Triggers** and hitting the **Revoke** button. The action is +irreversible. + +## Trigger a build + +To trigger a build you need to send a `POST` request to GitLab's API endpoint: + +``` +POST /projects/:id/trigger/builds +``` + +The required parameters are the trigger's `token` and the Git `ref` on which +the trigger will be performed. Valid refs are the branch, the tag or the commit +SHA. The `:id` of a project can be found by [querying the API](../api/projects.md) +or by visiting the **Triggers** page which provides self-explanatory examples. + +When a rebuild is triggered, the information is exposed in GitLab's UI under +the **Builds** page and the builds are marked as `triggered`. + + + +--- + +You can see which trigger caused the rebuild by visiting the single build page. +The token of the trigger is exposed in the UI as you can see from the image +below. + + + +--- + +See the [Examples](#examples) section for more details on how to actually +trigger a rebuild. + +## Pass build variables to a trigger + +You can pass any number of arbitrary variables in the trigger API call and they +will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml` +file. The parameter is of the form: + +``` +variables[key]=value +``` + +This information is also exposed in the UI. + + + +--- + +See the [Examples](#examples) section below for more details. + +## Examples + +Using cURL you can trigger a rebuild with minimal effort, for example: + +```bash +curl -X POST \ + -F token=TOKEN \ + -F ref=master \ + https://gitlab.example.com/api/v3/projects/9/trigger/builds +``` + +In this case, the project with ID `9` will get rebuilt on `master` branch. + + +### Triggering a build within `.gitlab-ci.yml` + +You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that +you have two projects, A and B, and you want to trigger a rebuild on the `master` +branch of project B whenever a tag on project A is created. This is the job you +need to add in project's A `.gitlab-ci.yml`: + +```yaml +build_docs: + stage: deploy + script: + - "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds" + only: + - tags +``` + +Now, whenever a new tag is pushed on project A, the build will run and the +`build_docs` job will be executed, triggering a rebuild of project B. The +`stage: deploy` ensures that this job will run only after all jobs with +`stage: test` complete successfully. + +_**Note:** If your project is public, passing the token in plain text is +probably not the wisest idea, so you might want to use a +[secure variable](../variables/README.md#user-defined-variables-secure-variables) +for that purpose._ + +### Making use of trigger variables + +Using trigger variables can be proven useful for a variety of reasons. + +* Identifiable jobs. Since the variable is exposed in the UI you can know + why the rebuild was triggered if you pass a variable that explains the + purpose. +* Conditional job processing. You can have conditional jobs that run whenever + a certain variable is present. + +Consider the following `.gitlab-ci.yml` where we set three +[stages](../yaml/README.md#stages) and the `upload_package` job is run only +when all jobs from the test and build stages pass. When the `UPLOAD_TO_S3` +variable is non-zero, `make upload` is run. + +```yaml +stages: +- test +- build +- package + +run_tests: + script: + - make test + +build_package: + stage: build + script: + - make build + +upload_package: + stage: package + script: + - if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi +``` + +You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable +and the script of the `upload_package` job will run: + +```bash +curl -X POST \ + -F token=TOKEN \ + -F ref=master \ + -F "variables[UPLOAD_TO_S3]=true" \ + https://gitlab.example.com/api/v3/projects/9/trigger/builds +``` + +### Using cron to trigger nightly builds + +Whether you craft a script or just run cURL directly, you can trigger builds +in conjunction with cron. The example below triggers a build on the `master` +branch of project with ID `9` every night at `00:30`: + +```bash +30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds +``` + +[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png new file mode 100644 index 0000000000000000000000000000000000000000..e78794fbee767d82382d2d97e7c9f91a9d9c09c7 Binary files /dev/null and b/doc/ci/triggers/img/builds_page.png differ diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png new file mode 100644 index 0000000000000000000000000000000000000000..c25f27409d65ce5548cd26940cc568c62fcd5425 Binary files /dev/null and b/doc/ci/triggers/img/trigger_single_build.png differ diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png new file mode 100644 index 0000000000000000000000000000000000000000..2207e8b34cbd6b42970e3e47e0740973e0ca655f Binary files /dev/null and b/doc/ci/triggers/img/trigger_variables.png differ diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png new file mode 100644 index 0000000000000000000000000000000000000000..268368dc3c5b7c6046c2c15f0819641699e9d9a6 Binary files /dev/null and b/doc/ci/triggers/img/triggers_page.png differ diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 022afb700424c25eab121cc08d00e667d4999e7f..b99ea25a3fe33ef74ceef3a9a4202452e897e95e 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -27,7 +27,6 @@ The API_TOKEN will take the Secure Variable value: `SECURE`. | **CI_BUILD_TAG** | 0.5 | The commit tag name. Present only when building tags. | | **CI_BUILD_NAME** | 0.5 | The name of the build as defined in `.gitlab-ci.yml` | | **CI_BUILD_STAGE** | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | -| **CI_BUILD_BEFORE_SHA** | all | The first commit that were included in push request | | **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built | | **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally | | **CI_BUILD_REPO** | all | The URL to clone the Git repository | @@ -40,7 +39,6 @@ The API_TOKEN will take the Secure Variable value: `SECURE`. Example values: ```bash -export CI_BUILD_BEFORE_SHA="9df57456fa9de2a6d335ca5edf9750ed812b9df0" export CI_BUILD_ID="50" export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a" export CI_BUILD_REF_NAME="master" diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7e2edb945da6035e586d7fbd7cf3e8c0cb2c07c5..fd0d49de4e438ba654ee160276733cb38768069d 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1,9 +1,12 @@ # Configuration of your builds with .gitlab-ci.yml -From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (**.gitlab-ci.yml**) for the project configuration. -It is placed in the root of your repository and contains definitions of how your project should be built. -The YAML file defines a set of jobs with constraints stating when they should be run. -The jobs are defined as top-level elements with a name and always have to contain the `script` clause: +From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) +file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root +of your repository and contains definitions of how your project should be built. + +The YAML file defines a set of jobs with constraints stating when they should +be run. The jobs are defined as top-level elements with a name and always have +to contain the `script` clause: ```yaml job1: @@ -13,15 +16,21 @@ job2: script: "execute-script-for-job2" ``` -The above example is the simplest possible CI configuration with two separate jobs, -where each of the jobs executes a different command. -Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository. +The above example is the simplest possible CI configuration with two separate +jobs, where each of the jobs executes a different command. + +Of course a command can execute code directly (`./configure;make;make install`) +or run a script (`test.sh`) in the repository. -Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner. -What is important, is that each job is run independently from each other. +Jobs are used to create builds, which are then picked up by +[runners](../runners/README.md) and executed within the environment of the +runner. What is important, is that each job is run independently from each +other. ## .gitlab-ci.yml -The YAML syntax allows for using more complex job specifications than in the above example: + +The YAML syntax allows for using more complex job specifications than in the +above example: ```yaml image: ruby:2.1 @@ -46,26 +55,31 @@ job1: - docker ``` -There are a few `keywords` that can't be used as job names: +There are a few reserved `keywords` that **cannot** be used as job names: -| keyword | required | description | +| Keyword | Required | Description | |---------------|----------|-------------| -| image | optional | Use docker image, covered in [Use Docker](../docker/README.md) | -| services | optional | Use docker services, covered in [Use Docker](../docker/README.md) | -| stages | optional | Define build stages | -| types | optional | Alias for `stages` | -| before_script | optional | Define commands prepended for each job's script | -| variables | optional | Define build variables | -| cache | optional | Define list of files that should be cached between subsequent runs | +| image | no | Use docker image, covered in [Use Docker](../docker/README.md) | +| services | no | Use docker services, covered in [Use Docker](../docker/README.md) | +| stages | no | Define build stages | +| types | no | Alias for `stages` | +| before_script | no | Define commands that run before each job's script | +| variables | no | Define build variables | +| cache | no | Define list of files that should be cached between subsequent runs | ### image and services -This allows to specify a custom Docker image and a list of services that can be used for time of the build. -The configuration of this feature is covered in separate document: [Use Docker](../docker/README.md). + +This allows to specify a custom Docker image and a list of services that can be +used for time of the build. The configuration of this feature is covered in +separate document: [Use Docker](../docker/README.md). ### before_script -`before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multiline string. + +`before_script` is used to define the command that should be run before all +builds, including deploy builds. This can be an array or a multi-line string. ### stages + `stages` is used to define build stages that can be used by jobs. The specification of `stages` allows for having flexible multi stage pipelines. @@ -75,7 +89,8 @@ The ordering of elements in `stages` defines the ordering of builds' execution: 1. Builds of next stage are run after success. Let's consider the following example, which defines 3 stages: -``` + +```yaml stages: - build - test @@ -86,21 +101,26 @@ stages: 1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel. 1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel. 1. If all jobs of `deploy` succeeds, the commit is marked as `success`. -1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. +1. If any of the previous jobs fails, the commit is marked as `failed` and no + jobs of further stage are executed. There are also two edge cases worth mentioning: -1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default. +1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, + `test` and `deploy` are allowed to be used as job's stage by default. 2. If a job doesn't specify `stage`, the job is assigned the `test` stage. ### types + Alias for [stages](#stages). ### variables -**This feature requires `gitlab-runner` with version equal or greater than 0.5.0.** -GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment. -The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL. +_**Note:** Introduced in GitLab Runner v0.5.0._ + +GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build +environment. The variables are stored in the git repository and are meant to +store non-sensitive project configuration, for example: ```yaml variables: @@ -109,18 +129,23 @@ variables: These variables can be later used in all executed commands and scripts. -The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. +The YAML-defined variables are also set to all created service containers, +thus allowing to fine tune them. ### cache -`cache` is used to specify list of files and directories which should be cached between builds. -Caches are stored according to the branch/ref and the job name. Caches are not -currently shared between different job names or between branches/refs. This means -caching will benefit you if you push subsequent commits to an existing feature branch. -**The global setting allows to specify default cached files for all jobs.** +`cache` is used to specify a list of files and directories which should be +cached between builds. Caches are stored according to the branch/ref and the +job name. They are not currently shared between different job names or between +branches/refs, which means that caching will benefit you if you push subsequent +commits to an existing feature branch. + +If `cache` is defined outside the scope of the jobs, it means it is set +globally and all jobs will use its definition. To cache all git untracked files and files in `binaries`: -``` + +```yaml cache: untracked: true paths: @@ -128,9 +153,10 @@ cache: ``` ## Jobs -`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. -Each job has to have a unique `job_name`, which is not one of the keywords mentioned above. -A job is defined by a list of parameters that define the build behaviour. + +`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job +must have a unique name, which is not one of the Keywords mentioned above. +A job is defined by a list of parameters that define the build behavior. ```yaml job_name: @@ -148,21 +174,22 @@ job_name: allow_failure: true ``` -| keyword | required | description | +| Keyword | Required | Description | |---------------|----------|-------------| -| script | required | Defines a shell script which is executed by runner | -| stage | optional (default: test) | Defines a build stage | -| type | optional | Alias for `stage` | -| only | optional | Defines a list of git refs for which build is created | -| except | optional | Defines a list of git refs for which build is not created | -| tags | optional | Defines a list of tags which are used to select runner | -| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | -| when | optional | Define when to run build. Can be `on_success`, `on_failure` or `always` | -| artifacts | optional | Define list build artifacts | -| cache | optional | Define list of files that should be cached between subsequent runs | +| script | yes | Defines a shell script which is executed by runner | +| stage | no (default: `test`) | Defines a build stage | +| type | no | Alias for `stage` | +| only | no | Defines a list of git refs for which build is created | +| except | no | Defines a list of git refs for which build is not created | +| tags | no | Defines a list of tags which are used to select runner | +| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | +| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | +| artifacts | no | Define list build artifacts | +| cache | no | Define list of files that should be cached between subsequent runs | ### script -`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. + +`script` is a shell script which is executed by the runner. For example: ```yaml job: @@ -170,6 +197,7 @@ job: ``` This parameter can also contain several commands using an array: + ```yaml job: script: @@ -178,31 +206,45 @@ job: ``` ### stage -`stage` allows to group build into different stages. Builds of the same `stage` are executed in `parallel`. -For more info about the use of `stage` please check the [stages](#stages). + +`stage` allows to group build into different stages. Builds of the same `stage` +are executed in `parallel`. For more info about the use of `stage` please check +[stages](#stages). ### only and except -This are two parameters that allow for setting a refs policy to limit when jobs are built: -1. `only` defines the names of branches and tags for which job will be built. -2. `except` defines the names of branches and tags for which the job wil **not** be built. -There are a few rules that apply to usage of refs policy: +`only` and `except` are two parameters that set a refs policy to limit when +jobs are built: -1. `only` and `except` are inclusive. If both `only` and `except` are defined in job specification the ref is filtered by `only` and `except`. -1. `only` and `except` allow for using the regexp expressions. -1. `only` and `except` allow for using special keywords: `branches` and `tags`. -These names can be used for example to exclude all tags and all branches. +1. `only` defines the names of branches and tags for which the job will be + built. +2. `except` defines the names of branches and tags for which the job will + **not** be built. + +There are a few rules that apply to the usage of refs policy: + +* `only` and `except` are inclusive. If both `only` and `except` are defined + in a job specification, the ref is filtered by `only` and `except`. +* `only` and `except` allow the use of regular expressions. +* `only` and `except` allow the use of special keywords: `branches` and `tags`. +* `only` and `except` allow to specify a repository path to filter jobs for + forks. + +In the example below, `job` will run only for refs that start with `issue-`, +whereas all branches will be skipped. ```yaml job: + # use regexp only: - - /^issue-.*$/ # use regexp + - /^issue-.*$/ + # use special keyword except: - - branches # use special keyword + - branches ``` -1. `only` and `except` allow for specify repository path to filter jobs for forks. -The repository path can be used to have jobs executed only for parent repository. +The repository path can be used to have jobs executed only for the parent +repository and not forks: ```yaml job: @@ -211,33 +253,47 @@ job: except: - master@gitlab-org/gitlab-ce ``` -The above will run `job` for all branches on `gitlab-org/gitlab-ce`, except master . + +The above example will run `job` for all branches on `gitlab-org/gitlab-ce`, +except master. ### tags -`tags` is used to select specific runners from the list of all runners that are allowed to run this project. -During registration of a runner, you can specify the runner's tags, ie.: `ruby`, `postgres`, `development`. -`tags` allow you to run builds with runners that have the specified tags assigned: +`tags` is used to select specific runners from the list of all runners that are +allowed to run this project. -``` +During the registration of a runner, you can specify the runner's tags, for +example `ruby`, `postgres`, `development`. + +`tags` allow you to run builds with runners that have the specified tags +assigned to them: + +```yaml job: tags: - ruby - postgres ``` -The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined. +The specification above, will make sure that `job` is built by a runner that +has both `ruby` AND `postgres` tags defined. ### when -`when` is used to implement jobs that are run in case of failure or despite the failure. + +`when` is used to implement jobs that are run in case of failure or despite the +failure. `when` can be set to one of the following values: -1. `on_success` - execute build only when all builds from prior stages succeeded. This is the default. -1. `on_failure` - execute build only when at least one build from prior stages failed. +1. `on_success` - execute build only when all builds from prior stages + succeeded. This is the default. +1. `on_failure` - execute build only when at least one build from prior stages + failed. 1. `always` - execute build despite the status of builds from prior stages. -``` +For example: + +```yaml stages: - build - cleanup_build @@ -245,28 +301,28 @@ stages: - deploy - cleanup -build: +build_job: stage: build script: - make build -cleanup_build: +cleanup_build_job: stage: cleanup_build script: - cleanup build when failed when: on_failure -test: +test_job: stage: test script: - make test -deploy: +deploy_job: stage: deploy script: - make deploy -cleanup: +cleanup_job: stage: cleanup script: - cleanup after builds @@ -274,84 +330,108 @@ cleanup: ``` The above script will: -1. Execute `cleanup_build` only when the `build` failed, -2. Always execute `cleanup` as the last step in pipeline. + +1. Execute `cleanup_build_job` only when `build_job` fails +2. Always execute `cleanup_job` as the last step in pipeline. ### artifacts -`artifacts` is used to specify list of files and directories which should be attached to build after success. -1. Send all files in `binaries` and `.config`: +_**Note:** Introduced in GitLab Runner v0.7.0. Also, the Windows shell executor + does not currently support artifact uploads._ - artifacts: - paths: - - binaries/ - - .config +`artifacts` is used to specify list of files and directories which should be +attached to build after success. Below are some examples. -2. Send all git untracked files: +Send all files in `binaries` and `.config`: - artifacts: - untracked: true +```yaml +artifacts: + paths: + - binaries/ + - .config +``` -3. Send all git untracked files and files in `binaries`: +Send all git untracked files: - artifacts: - untracked: true - paths: - - binaries/ +```yaml +artifacts: + untracked: true +``` + +Send all git untracked files and files in `binaries`: -The artifacts will be send after the build success to GitLab and will be accessible in GitLab interface to download. +```yaml +artifacts: + untracked: true + paths: + - binaries/ +``` -This feature requires GitLab Runner v0.7.0 or higher. +The artifacts will be send after a successful build success to GitLab, and will +be accessible in the GitLab UI to download. ### cache -`cache` is used to specify list of files and directories which should be cached between builds. -1. Cache all files in `binaries` and `.config`: +_**Note:** Introduced in GitLab Runner v0.7.0._ - rspec: - script: test - cache: - paths: - - binaries/ - - .config +`cache` is used to specify list of files and directories which should be cached +between builds. Below are some examples: -2. Cache all git untracked files: +Cache all files in `binaries` and `.config`: - rspec: - script: test - cache: - untracked: true - -3. Cache all git untracked files and files in `binaries`: +```yaml +rspec: + script: test + cache: + paths: + - binaries/ + - .config +``` - rspec: - script: test - cache: - untracked: true - paths: - - binaries/ +Cache all git untracked files: -4. Locally defined cache overwrites globally defined options. This will cache only `binaries/`: +```yaml +rspec: + script: test + cache: + untracked: true +``` + +Cache all git untracked files and files in `binaries`: + +```yaml +rspec: + script: test + cache: + untracked: true + paths: + - binaries/ +``` - cache: - paths: - - my/files - - rspec: - script: test - cache: - paths: - - binaries/ +Locally defined cache overwrites globally defined options. This will cache only +`binaries/`: -The cache is provided on best effort basis, so don't expect that cache will be present. -For implementation details please check GitLab Runner. +```yaml +cache: + paths: + - my/files -This feature requires GitLab Runner v0.7.0 or higher. +rspec: + script: test + cache: + paths: + - binaries/ +``` +The cache is provided on best effort basis, so don't expect that cache will be +always present. For implementation details please check GitLab Runner. ## Validate the .gitlab-ci.yml + Each instance of GitLab CI has an embedded debug tool called Lint. -You can find the link to the Lint in the project's settings page or use short url `/lint`. +You can find the link under `/ci/lint` of your gitlab instance. ## Skipping builds -There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped + +If your commit message contains `[ci skip]`, the commit will be created but the +builds will be skipped. diff --git a/doc/install/installation.md b/doc/install/installation.md index f8116a8a31c2b9afa199cfda8e84057d16eb6052..81edd8da2b845f055bba5f964dc6b3c98a979ac5 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout 0.4.2 + sudo -u git -H git checkout 0.5.1 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/integration/README.md b/doc/integration/README.md index eff39a626ae6205a122558fd5f8990882d930b04..2a9f76533b733a224af1389fef13caf00dac73ef 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -4,13 +4,16 @@ GitLab integrates with multiple third-party services to allow external issue tra See the documentation below for details on how to configure these services. +- [Jira](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [CAS](cas.md) Configure GitLab to sign in using CAS - [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages +- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). diff --git a/doc/integration/cas.md b/doc/integration/cas.md new file mode 100644 index 0000000000000000000000000000000000000000..e6b2071f193cd5ca5e424960f4ac9a198de7fbcd --- /dev/null +++ b/doc/integration/cas.md @@ -0,0 +1,62 @@ +# CAS OmniAuth Provider + +To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab will supply to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for backchannel logout. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: "cas3", + label: "cas", + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'cas3', + label: 'cas', + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + ``` + +1. Change 'CAS_PATH' to the root of your CAS instance (ie. `cas`). + +1. If your CAS instance does not use default TGC lifetimes, update the `cas3.session_duration` to at least the current TGC maximum lifetime. To explicitly disable SLO, regardless of CAS settings, set this to 0. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a CAS tab in the sign in form. diff --git a/doc/integration/jira.md b/doc/integration/jira.md new file mode 100644 index 0000000000000000000000000000000000000000..624601d0faccb8e0192d7d7865168a9da0798255 --- /dev/null +++ b/doc/integration/jira.md @@ -0,0 +1,113 @@ +# GitLab Jira integration + +GitLab can be configured to interact with Jira. +Configuration happens via username and password. +Connecting to a Jira server via CAS is not possible. + +Each project can be configured to connect to a different Jira instance, configuration is explained [here](#configuration). +If you have one Jira instance you can pre-fill the settings page with a default template. To configure the template [see external issue tracker document](external-issue-tracker.md#service-template)). + +Once the project is connected to Jira, you can reference and close the issues in Jira directly from GitLab. + + +## Table of Contents + +* [Referencing Jira Issues from GitLab](#referencing-jira-issues) +* [Closing Jira Issues from GitLab](#closing-jira-issues) +* [Configuration](#configuration) + +### Referencing Jira Issues + +When GitLab project has Jira issue tracker configured and enabled, mentioning Jira issue in GitLab will automatically add a comment in Jira issue with the link back to GitLab. This means that in comments in merge requests and commits referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the format: + + +``` + USER mentioned this issue in LINK_TO_THE_MENTION +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned. +Can be commit or merge request. + + + + + +### Closing Jira Issues + +Jira issues can be closed directly from GitLab by using trigger words, eg. `Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and merge requests. +When a commit which contains the trigger word in the commit message is pushed, GitLab will add a comment in the mentioned Jira issue. + +For example, for project named PROJECT in Jira, we implemented a new feature and created a merge request in GitLab. + +This feature was requested in Jira issue PROJECT-7. Merge request in GitLab contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, Jira issue will be automatically closed with a link to the commit that resolved the issue. + + + + + + + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab. +Login to your JIRA instance as admin and under Administration go to User Management and create a new user. +As an example, we'll create a user named `gitlab` and add it to `jira-developers` group. + +**It is important that the user `gitlab` has write-access to projects in JIRA** + +### Configuring GitLab + +### GitLab 7.8 EE and up with JIRA v6.x + +To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA. + +Fill in the required details on the page: + + + +* `description` A name for the issue tracker (to differentiate between instances, for instance). +* `project url` The URL to the JIRA project which is being linked to this GitLab project. +* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project. +* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project. +* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`. +* `username` The username of the user created in [configuring JIRA step](#configuring-jira). +* `password` The password of the user created in [configuring JIRA step](#configuring-jira). +* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well) + +After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. + + +### GitLab 6.x-7.7 with JIRA v6.x + +**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.** + + +In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115). +This will make sure that all issues within GitLab are pointing to the JIRA issue tracker. + +We can also enable JIRA service that will allow us to interact with JIRA issues. + +For example, we can close issues in JIRA by a commit in GitLab. + +Go to project settings page and fill in the project name for the JIRA project: + + + +Next, go to the services page and find JIRA. + + + +1. Tick the active check box to enable the service. +1. Supply the url to JIRA server, for example http://jira.sample +1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA api version, default is version +1. Optional: supply the JIRA issue transition ID (issue transition to closed). This is dependant on JIRA settings, default is 2 +1. Save + +Now we should be able to interact with JIRA issues. diff --git a/doc/integration/jira_issue_reference.png b/doc/integration/jira_issue_reference.png new file mode 100644 index 0000000000000000000000000000000000000000..15739a22dc7151af663316c1d44e8fcda704ce29 Binary files /dev/null and b/doc/integration/jira_issue_reference.png differ diff --git a/doc/integration/jira_project_name.png b/doc/integration/jira_project_name.png new file mode 100644 index 0000000000000000000000000000000000000000..5986fdb63fb8727b339b69d7fa6b64d3ef5f4b17 Binary files /dev/null and b/doc/integration/jira_project_name.png differ diff --git a/doc/integration/jira_service.png b/doc/integration/jira_service.png new file mode 100644 index 0000000000000000000000000000000000000000..1f6628c43719edd55d415f80d28d8d4423002586 Binary files /dev/null and b/doc/integration/jira_service.png differ diff --git a/doc/integration/jira_service_close_issue.png b/doc/integration/jira_service_close_issue.png new file mode 100644 index 0000000000000000000000000000000000000000..67dfc6144c44f0643e1373bef9961bf0c3a458fb Binary files /dev/null and b/doc/integration/jira_service_close_issue.png differ diff --git a/doc/integration/jira_service_page.png b/doc/integration/jira_service_page.png new file mode 100644 index 0000000000000000000000000000000000000000..69ec44e826fb82702103f7cf1fd4299084e24151 Binary files /dev/null and b/doc/integration/jira_service_page.png differ diff --git a/doc/integration/jira_workflow_screenshot.png b/doc/integration/jira_workflow_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8635a32eb6874fa64bc53ba671794c20e9df2f00 Binary files /dev/null and b/doc/integration/jira_workflow_screenshot.png differ diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md new file mode 100644 index 0000000000000000000000000000000000000000..a301d1a613c3a8e8800911fd0a48a41bccb1f3e3 --- /dev/null +++ b/doc/integration/recaptcha.md @@ -0,0 +1,23 @@ +# reCAPTCHA + +GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/index.html) +to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page +to confirm that a real user, not a bot, is attempting to create an account. + +## Configuration + +To use reCAPTCHA, first you must create a site and private key. + +1. Go to the URL: https://www.google.com/recaptcha/admin + +2. Fill out the form necessary to obtain reCAPTCHA keys. + +3. Login to your GitLab server, with administrator credentials. + +4. Go to Applications Settings on Admin Area (`admin/application_settings`) + +5. Fill all recaptcha fields with keys from previous steps + +6. Check the `Enable reCAPTCHA` checkbox + +7. Save the configuration. diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index bcd00cfc6bf82f44c19fdc96980a6c19377bbfa7..1be78ac1823f4470c40d3e1148ffcd233f25f4af 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,11 +6,11 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. -On public projects the Guest role is not enforced. -All users will be able to create issues, leave comments, and pull or download the project code. +On public projects the Guest role is not enforced. +All users will be able to create issues, leave comments, and pull or download the project code. To add or import a user, you can follow the [project users and members -documentation](doc/workflow/add-user/add-user.md). +documentation](../workflow/add-user/add-user.md). ## Project diff --git a/doc/security/README.md b/doc/security/README.md index fba6013d9c153ac2d91aa7252dfe547aaab299a7..f34c792d00549596d2860c18f2985d84bca37090 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -6,3 +6,5 @@ - [Information exclusivity](information_exclusivity.md) - [Reset your root password](reset_root_password.md) - [User File Uploads](user_file_uploads.md) +- [How we manage the CRIME vulnerability](crime_vulnerability.md) +- [Enforce Two-Factor authentication](two_factor_authentication.md) diff --git a/doc/security/crime_vulnerability.md b/doc/security/crime_vulnerability.md new file mode 100644 index 0000000000000000000000000000000000000000..94ba5d1375dd550178deecce20d36851054119a5 --- /dev/null +++ b/doc/security/crime_vulnerability.md @@ -0,0 +1,63 @@ +# How we manage the TLS protocol CRIME vulnerability + +> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against +secret web cookies over connections using the HTTPS and SPDY protocols that also +use data compression. When used to recover the content of secret +authentication cookies, it allows an attacker to perform session hijacking on an +authenticated web session, allowing the launching of further attacks. +([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806)) + +### Description + +The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore +it warns against using SSL Compression (for example gzip) or SPDY which +optionally uses compression as well. + +GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME +vulnerability by deactivating gzip when HTTPS is enabled. You can see the +sources of the files in question: + +* [Source installation NGINX file][source-nginx] +* [Omnibus installation NGINX file][omnibus-nginx] + +Although SPDY is enabled in Omnibus installations, CRIME relies on compression +(the 'C') and the default compression level in NGINX's SPDY module is 0 +(no compression). + +### Nessus + +The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab +similar to the following format: + +``` +Description + +This remote service has one of two configurations that are known to be required for the CRIME attack: +SSL/TLS compression is enabled. +TLS advertises the SPDY protocol earlier than version 4. + +... + +Output + +The following configuration indicates that the remote service may be vulnerable to the CRIME attack: +SPDY support earlier than version 4 is advertised. +``` + +From the report above it is important to note that Nessus is only checking if +TLS advertises the SPDY protocol earlier than version 4, it does not perform an +attack nor does it check if compression is enabled. With just this approach, it +cannot tell that SPDY's compression is disabled and not subject to the CRIME +vulnerability. + +### References + +* Nginx ["Module ngx_http_spdy_module"][ngx-spdy] +* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus] +* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia + +[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl +[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb +[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html +[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565 +[wiki-crime]: https://en.wikipedia.org/wiki/CRIME diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..4e25a1fdc3ff381c271726b968da2e01d0b97f79 --- /dev/null +++ b/doc/security/two_factor_authentication.md @@ -0,0 +1,38 @@ +# Enforce Two-factor Authentication (2FA) + +Two-factor Authentication (2FA) provides an additional level of security to your +users' GitLab account. Once enabled, in addition to supplying their username and +password to login, they'll be prompted for a code generated by an application on +their phone. + +You can read more about it here: +[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md) + +## Enabling 2FA + +Users on GitLab, can enable it without any admin's intervention. If you want to +enforce everyone to setup 2FA, you can choose from two different ways: + + 1. Enforce on next login + 2. Suggest on next login, but allow a grace period before enforcing. + +In the Admin area under **Settings** (`/admin/application_settings`), look for +the "Sign-in Restrictions" area, where you can configure both. + +If you want 2FA enforcement to take effect on next login, change the grace +period to `0` + +## Disabling 2FA for everyone + +There may be some special situations where you want to disable 2FA for everyone +even when forced 2FA is disabled. There is a rake task for that: + +``` +# use this command if you've installed GitLab with the Omnibus package +sudo gitlab-rake gitlab:two_factor:disable_for_all_users + +# if you've installed GitLab from source +sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production +``` + +**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.** diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md index e69c4f7ed3c2da2ec9330ceb54ee37e7c57ff49f..3748941b7815a93c9de94fe1ace36496e1d01b29 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -67,7 +67,7 @@ sudo -u git -H git checkout 8-3-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all -sudo -u git -H git checkout v2.6.8 +sudo -u git -H git checkout v2.6.9 ``` ### 5. Update gitlab-workhorse @@ -78,7 +78,7 @@ which should already be on your system from GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout 0.4.2 +sudo -u git -H git checkout 0.5.1 sudo -u git -H make ``` @@ -99,8 +99,6 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production # Clean up assets and cache sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production -# Update init.d script -sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab ``` ### 7. Update configuration files @@ -115,6 +113,12 @@ git diff origin/8-2-stable:config/gitlab.yml.example origin/8-3-stable:config/gi #### Nginx configuration +GitLab 8.3 introduces major changes in the NGINX configuration. +Because all HTTP requests pass through gitlab-workhorse now a lot of +directives need to be removed from NGINX. During future upgrades there +should be much less changes in the NGINX configuration because of +this. + View changes between the previous recommended Nginx configuration and the current one: @@ -134,6 +138,18 @@ via [/etc/default/gitlab]. [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/lib/support/init.d/gitlab.default.example#L34 +#### Init script + +We updated the init script for GitLab in order to pass new +configuration options to gitlab-workhorse. We let gitlab-workhorse +connect to the Rails application via a Unix domain socket and we tell +it where the 'public' directory of GitLab is. + +``` +cd /home/git/gitlab +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + ### 8. Use Redis v2.8.0+ Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but diff --git a/doc/workflow/README.md b/doc/workflow/README.md index d2642495c9a45938e6f31c2415ba4b9bbf90aa11..3651b55f438ee3c9d45e56e6daf69dd5e8c6a3b3 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -19,3 +19,4 @@ - ["Work In Progress" Merge Requests](wip_merge_requests.md) - [Merge When Build Succeeds](merge_when_build_succeeds.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) +- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md) diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 7ccf06fbd60b8ef634dc98ce93c1537f9af44e76..18e5d950866a4c16f70a5155848bce9b74f0f80d 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -1,13 +1,17 @@ # Migrating projects to a GitLab instance 1. [Bitbucket](import_projects_from_bitbucket.md) -2. [GitHub](import_projects_from_github.md) -3. [GitLab.com](import_projects_from_gitlab_com.md) -4. [FogBugz](import_projects_from_fogbugz.md) -4. [SVN](migrating_from_svn.md) +1. [GitHub](import_projects_from_github.md) +1. [GitLab.com](import_projects_from_gitlab_com.md) +1. [FogBugz](import_projects_from_fogbugz.md) +1. [SVN](migrating_from_svn.md) -### Note -* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. +In addition to the specific migration documentation above, you can import any +Git repository via HTTP from the New Project page. Be aware that if the +repository is too large the import can timeout. + +### Migrating from self-hosted GitLab to GitLab.com + +You can copy your repos by changing the remote and pushing to the new server; +but issues and merge requests can't be imported. -* You can import any Git repository via HTTP from the New Project page. -If the repository is too large, it can timeout. diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 1938ccd0c264c42b2d42b7772f85980e1d1d262d..b355a91b5a6e693ba060a5b49beca7ae0c9c635d 100644 --- a/doc/workflow/importing/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md @@ -1,17 +1,78 @@ # Migrating from SVN to GitLab -SVN stands for Subversion and is a version control system (VCS). -Git is a distributed version control system. +Subversion (SVN) is a central version control system (VCS) while +Git is a distributed version control system. There are some major differences +between the two, for more information consult your favorite search engine. -There are some major differences between the two, for more information consult your favorite search engine. +If you are currently using an SVN repository, you can migrate the repository +to Git and GitLab. We recommend a hard cut over - run the migration command once +and then have all developers start using the new GitLab repository immediately. +Otherwise, it's hard to keep changing in sync in both directions. The conversion +process should be run on a local workstation. -Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at -[git documentation pages](https://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion). +Install `svn2git`. On all systems you can install as a Ruby gem if you already +have Ruby and Git installed. -Apart from the [official git documentation](https://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also -user created step by step guide for migrating from SVN to GitLab. +```bash +sudo gem install svn2git +``` -[Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca). +On Debian-based Linux distributions you can install the native packages: + +```bash +sudo apt-get install git-core git-svn ruby +``` + +Optionally, prepare an authors file so `svn2git` can map SVN authors to Git authors. +If you choose not to create the authors file then commits will not be attributed +to the correct GitLab user. Some users may not consider this a big issue while +others will want to ensure they complete this step. If you choose to map authors +you will be required to map every author that is present on changes in the SVN +repository. If you don't, the conversion will fail and you will have to update +the author file accordingly. The following command will search through the +repository and output a list of authors. + +```bash +svn log --quiet | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq +``` + +Use the output from the last command to construct the authors file. +Create a file called `authors.txt` and add one mapping per line. + +``` +janedoe = Jane Doe <janedoe@example.com> +johndoe = John Doe <johndoe@example.com> +``` + +If your SVN repository is in the standard format (trunk, branches, tags, +not nested) the conversion is simple. For a non-standard repository see +[svn2git documentation](https://github.com/nirvdrum/svn2git). The following +command will checkout the repository and do the conversion in the current +working directory. Be sure to create a new directory for each repository before +running the `svn2git` command. The conversion process will take some time. + +```bash +svn2git https://svn.example.com/path/to/repo --authors /path/to/authors.txt +``` + +If your SVN repository requires a username and password add the +`--username <username>` and `--password <password` flags to the above command. +`svn2git` also supports excluding certain file paths, branches, tags, etc. See +[svn2git documentation](https://github.com/nirvdrum/svn2git) or run +`svn2git --help` for full documentation on all of the available options. + +Create a new GitLab project, where you will eventually push your converted code. +Copy the SSH or HTTP(S) repository URL from the project page. Add the GitLab +repository as a Git remote and push all the changes. This will push all commits, +branches and tags. + +```bash +git remote add origin git@gitlab.com:<group>/<project>.git +git push --all origin +``` ## Contribute to this guide -We welcome all contributions that would expand this guide with instructions on how to migrate from SVN and other version control systems. +We welcome all contributions that would expand this guide with instructions on +how to migrate from SVN and other version control systems. + + diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 5103ca1294756abebba39fd1c97034612ff274ec..2c17d32154a9788bcb168213358aa305e506c154 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -25,6 +25,7 @@ Feature: Project Commits Branches And I click branch 'improve/awesome' delete link Then I should not see branch 'improve/awesome' + @javascript Scenario: I create a branch with invalid name Given I visit project branches page And I click new branch link diff --git a/features/project/create.feature b/features/project/create.feature index a86079143e58ad86233cd5c37ff71de859d8def2..27136798e36d14f9b2b55a306f8b8bc30b189297 100644 --- a/features/project/create.feature +++ b/features/project/create.feature @@ -1,3 +1,4 @@ +@project-create Feature: Project Create In order to get access to project sections A user with ability to create a project diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index 0ce99e855c6f91b0c8d0c16af8f065963647146e..9a06fdc2ee6eb5ea0a40a84ea441a31cc3b5ebdc 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -13,7 +13,18 @@ Feature: Award Emoji Then I have award added And I can remove it by clicking to icon + @javascript + Scenario: I can see the list of emoji categories + Given I click to emoji-picker + Then I can see the activity and food categories + + @javascript + Scenario: I can search emoji + Given I click to emoji-picker + And I search "hand" + Then I see search result for "hand" + @javascript Scenario: I add award emoji using regular comment - Given I leave comment with a single emoji - Then I have award added + Given I leave comment with a single emoji + Then I have award added diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature index d5e4f2b0bd8c4755d616fd5a76b44574cd1d9be1..330ec8ae0fe552cc9b4480525724626135a584d8 100644 --- a/features/project/merge_requests/accept.feature +++ b/features/project/merge_requests/accept.feature @@ -12,6 +12,14 @@ Feature: Project Merge Requests Acceptance Then I should see merge request merged And I should not see the Remove Source Branch button + @javascript + Scenario: Accepting the Merge Request when URL has an anchor + Given I am on the Merge Request detail with note anchor page + When I click on "Remove source branch" option + And I click on Accept Merge Request + Then I should see merge request merged + And I should not see the Remove Source Branch button + @javascript Scenario: Accepting the Merge Request without removing the source branch Given I am on the Merge Request detail page diff --git a/features/project/service.feature b/features/project/service.feature index ff3e7a0b38e6bd063878f3246d9806c8aa876275..3a7b830852489d2560619a4aabc6203023827cff 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -55,6 +55,12 @@ Feature: Project Services And I fill email on push settings Then I should see email on push service settings saved + Scenario: Activate JIRA service + When I visit project "Shop" services page + And I click jira service link + And I fill jira settings + Then I should see jira service settings saved + Scenario: Activate Irker (IRC Gateway) service When I visit project "Shop" services page And I click Irker service link diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 02159ee3776e9dafa46905e7f0bb248946b31ba8..a8c276b949ea51f9b928c72acdf6d5b4fd414e0b 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -24,6 +24,12 @@ Feature: Project Source Browse Files Given I click on "New file" link in repo Then I can see new file page + Scenario: I can create file when I don't have write access + Given I don't have write access + And I click on "New file" link in repo + Then I should see a notice about a new fork having been created + Then I can see new file page + @javascript Scenario: I can create and commit file Given I click on "New file" link in repo @@ -34,6 +40,17 @@ Feature: Project Source Browse Files Then I am redirected to the new file And I should see its new content + @javascript + Scenario: I can create and commit file when I don't have write access + Given I don't have write access + And I click on "New file" link in repo + And I edit code + And I fill the new file name + And I fill the commit message + And I click on "Commit Changes" + Then I am redirected to the fork's new merge request page + And I can see the new commit message + @javascript Scenario: I can create and commit file with new lines at the end of file Given I click on "New file" link in repo @@ -45,6 +62,17 @@ Feature: Project Source Browse Files And I click button "Edit" And I should see its content with new lines preserved at end of file + @javascript + Scenario: I can create and commit file and specify new branch + Given I click on "New file" link in repo + And I edit code + And I fill the new file name + And I fill the commit message + And I fill the new branch name + And I click on "Commit Changes" + Then I am redirected to the new merge request page + And I should see its new content + @javascript Scenario: I can upload file and commit Given I click on "Upload file" link in repo @@ -56,6 +84,19 @@ Feature: Project Source Browse Files And I am redirected to the new merge request page And I can see the new commit message + @javascript + Scenario: I can upload file and commit when I don't have write access + Given I don't have write access + And I click on "Upload file" link in repo + Then I should see a notice about a new fork having been created + When I click on "Upload file" link in repo + And I upload a new text file + And I fill the upload file commit message + And I click on "Upload file" + Then I can see the new text file + And I am redirected to the fork's new merge request page + And I can see the new commit message + @javascript Scenario: I can replace file and commit Given I click on ".gitignore" file in repo @@ -68,15 +109,19 @@ Feature: Project Source Browse Files And I can see the replacement commit message @javascript - Scenario: I can create and commit file and specify new branch - Given I click on "New file" link in repo - And I edit code - And I fill the new file name - And I fill the commit message - And I fill the new branch name - And I click on "Commit Changes" - Then I am redirected to the new merge request page - And I should see its new content + Scenario: I can replace file and commit when I don't have write access + Given I don't have write access + And I click on ".gitignore" file in repo + And I see the ".gitignore" + And I click on "Replace" + Then I should see a notice about a new fork having been created + When I click on "Replace" + And I replace it with a text file + And I fill the replace file commit message + And I click on "Replace file" + Then I can see the new text file + And I am redirected to the fork's new merge request page + And I can see the replacement commit message @javascript Scenario: I can create file in empty repo @@ -117,6 +162,14 @@ Feature: Project Source Browse Files And I click button "Edit" Then I can edit code + @javascript + Scenario: I can edit file when I don't have write access + Given I don't have write access + And I click on ".gitignore" file in repo + And I click button "Edit" + Then I should see a notice about a new fork having been created + And I can edit code + Scenario: If the file is binary the edit link is hidden Given I visit a binary file in the repo Then I cannot see the edit button @@ -131,6 +184,17 @@ Feature: Project Source Browse Files Then I am redirected to the ".gitignore" And I should see its new content + @javascript + Scenario: I can edit and commit file when I don't have write access + Given I don't have write access + And I click on ".gitignore" file in repo + And I click button "Edit" + And I edit code + And I fill the commit message + And I click on "Commit Changes" + Then I am redirected to the fork's new merge request page + And I can see the new commit message + @javascript Scenario: I can edit and commit file to new branch Given I click on ".gitignore" file in repo @@ -161,6 +225,17 @@ Feature: Project Source Browse Files And I click on "Create directory" Then I am redirected to the new merge request page + @javascript + Scenario: I can create directory in repo when I don't have write access + Given I don't have write access + When I click on "New directory" link in repo + Then I should see a notice about a new fork having been created + When I click on "New directory" link in repo + And I fill the new directory name + And I fill the commit message + And I click on "Create directory" + Then I am redirected to the fork's new merge request page + @javascript Scenario: I attempt to create an existing directory When I click on "New directory" link in repo @@ -188,6 +263,19 @@ Feature: Project Source Browse Files Then I am redirected to the files URL And I don't see the ".gitignore" + @javascript + Scenario: I can delete file and commit when I don't have write access + Given I don't have write access + And I click on ".gitignore" file in repo + And I see the ".gitignore" + And I click on "Delete" + Then I should see a notice about a new fork having been created + When I click on "Delete" + And I fill the commit message + And I click on "Delete file" + Then I am redirected to the fork's new merge request page + And I can see the new commit message + Scenario: I can browse directory with Browse Dir Given I click on files directory And I click on History link diff --git a/features/project/star.feature b/features/project/star.feature index a45f9c470ea8921eb2f023899752af0af6e83d38..618f44fe6dc31cc2a4ce916debbcca2a85cfc233 100644 --- a/features/project/star.feature +++ b/features/project/star.feature @@ -1,3 +1,4 @@ +@project-stars Feature: Project Star Scenario: New projects have 0 stars Given public project "Community" diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index 87cd33c37eb2d187556e1d7222dcb803ba96cdcd..87f32e70d59257c2b9365c692451d12ad55848d1 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -75,18 +75,18 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps name: projectname, path: "#{groupname}-#{projectname}", visibility_level: visibility_level - ) + ) create(:issue, title: "#{projectname} feature", project: project - ) + ) create(:merge_request, title: "#{projectname} feature implemented", source_project: project, target_project: project - ) + ) create(:closed_issue_event, project: project - ) + ) end end diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index f819dec219231e5fd8eede68794a500fed569244..742ba5d71f66680adc64c3ea41da634543258509 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -61,11 +61,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps create(:issue, title: "Bug", project: public_project - ) + ) create(:issue, title: "New feature", project: public_project - ) + ) visit namespace_project_issues_path(public_project.namespace, public_project) end @@ -80,11 +80,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps create(:issue, title: "Internal Bug", project: internal_project - ) + ) create(:issue, title: "New internal feature", project: internal_project - ) + ) visit namespace_project_issues_path(internal_project.namespace, internal_project) end @@ -104,7 +104,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "Bug fix for public project", source_project: public_project, target_project: public_project, - ) + ) end step 'I should see list of merge requests for "Community" project' do @@ -121,7 +121,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "Feature implemented", source_project: internal_project, target_project: internal_project - ) + ) end step 'I should see list of merge requests for "Internal" project' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index f5e3fee61c0f08fcd26677b5a0c20106b25bb91a..4c5122d1b7d505b9d0afe2dae55972cf3e63a541 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -85,7 +85,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see new group "Owned" avatar' do expect(owned_group.avatar).to be_instance_of AvatarUploader - expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif" + expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name:"Owned").id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 40b2aa7c357761bde578679b678d1bf39a1cb6ae..0305f7e6da0bc76e7ae831e86238505a7a3115c8 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -34,7 +34,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I should see new avatar' do expect(@user.avatar).to be_instance_of AvatarUploader - expect(@user.avatar.url).to eq "/uploads/user/avatar/#{ @user.id }/banana_sample.gif" + expect(@user.avatar.url).to eq "/uploads/user/avatar/#{@user.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 338f5e8d3ee66ce7c2ed50bf4f3695b2e7a6e614..0a42931147dc1ee7f78562f97c6fe1172ec1fec1 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -61,7 +61,8 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps end step 'I should see new an error that branch is invalid' do - expect(page).to have_content 'Branch name invalid' + expect(page).to have_content 'Branch name is invalid' + expect(page).to have_content "can't contain spaces" end step 'I should see new an error that ref is invalid' do diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index f90218f379162380cdc78d46168dd27ca63a7b72..8a0e8fc2b6c8998937c4140558cc882e877d5a20 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -26,7 +26,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'I click on HTTP' do - click_button 'HTTP' + find('#clone-dropdown').click + find('#http-selector').click end step 'Remote url should update to http link' do @@ -34,7 +35,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps end step 'If I click on SSH' do - click_button 'SSH' + find('#clone-dropdown').click + find('#ssh-selector').click end step 'Remote url should update to ssh link' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index d3675060994c820d0b1fd67838ed4e8f0e6f319d..cbdce78dc0ca300923c36096b632463d35821e09 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -41,7 +41,8 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps click_button "Compare branches and continue" - expect(page).to have_content "New Merge Request" + expect(page).to have_css("h3.page-title", text: "New Merge Request") + fill_in "merge_request_title", with: "Merge Request On Forked Project" end diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 325eaf2ea6a5834ea388eed6e1244e152b372a88..2c2ed08655ee722e077139a9e27eeeaca5d9660d 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -15,22 +15,31 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end step 'I click to emoji in the picker' do - page.within '.awards-menu' do - page.first('img').click + page.within '.emoji-menu-content' do + page.first('.emoji-icon').click end end step 'I can remove it by clicking to icon' do page.within '.awards' do - page.first('.award').click - expect(page).to_not have_selector '.award' + expect do + page.find('.award.active').click + sleep 0.3 + end.to change{ page.all(".award").size }.from(3).to(2) + end + end + + step 'I can see the activity and food categories' do + page.within '.emoji-menu' do + expect(page).to_not have_selector 'Activity' + expect(page).to_not have_selector 'Food' end end step 'I have award added' do page.within '.awards' do expect(page).to have_selector '.award' - expect(page.find('.award .counter')).to have_content '1' + expect(page.find('.award.active .counter')).to have_content '1' end end @@ -45,4 +54,16 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps click_button 'Add Comment' end end + + step 'I search "hand"' do + page.within('.emoji-menu-content') do + fill_in 'emoji_search', with: 'hand' + end + end + + step 'I see search result for "hand"' do + page.within '.emoji-menu-content' do + expect(page).to have_selector '[data-emoji="raised_hand"]' + end + end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 4a7ff21d385334f159f83034dea163727efa9ee8..8e8c9c57452f5218de00570885a90de15efaa855 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -59,15 +59,14 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click "author" dropdown' do - first('.ajax-users-select').click + first('#s2id_author_id').click end step 'I see current user as the first user' do - expect(page).to have_selector('.user-result', visible: true, count: 4) + expect(page).to have_selector('.user-result', visible: true, count: 3) users = page.all('.user-name') - expect(users[0].text).to eq 'Any Assignee' - expect(users[1].text).to eq 'Unassigned' - expect(users[2].text).to eq current_user.name + expect(users[0].text).to eq 'Any Author' + expect(users[1].text).to eq current_user.name end step 'I submit new issue "500 error on profile"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 0d340d97ff915a2893fa916e816d634ecaaa57fc..be993d11093edf42d08122f946436980361575b4 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -273,7 +273,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see merged request' do - page.within '.issue-box' do + page.within '.status-box' do expect(page).to have_content "Merged" end end @@ -283,7 +283,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see reopened merge request "Bug NS-04"' do - page.within '.issue-box' do + page.within '.status-box' do expect(page).to have_content "Open" end end diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 383c055c4efeb7926a7164e65eb557afc4b0563e..2685f5fd6b4b5f891fe4fb75afb7128d55fbd0f2 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -6,6 +6,10 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps visit merge_request_path(@merge_request) end + step 'I am on the Merge Request detail with note anchor page' do + visit merge_request_path(@merge_request, anchor: 'note_123') + end + step 'I click on "Remove source branch" option' do check('Remove source branch') end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 9ca7c8ebbc7ef8ed8efcbeb3f7f137d14e16c5eb..37bf52b4a95ac1eda2d568a60fd24015c33d6f86 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -37,7 +37,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should see new project avatar' do expect(@project.avatar).to be_instance_of AvatarUploader url = @project.avatar.url - expect(url).to eq "/uploads/project/avatar/#{ @project.id }/banana_sample.gif" + expect(url).to eq "/uploads/project/avatar/#{@project.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index ed3957ca87364b9b3cbd75b1f62b3ca2d10333b0..536199ddb4fd7c25f766b7a93b5a74dd992490e0 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -173,6 +173,24 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps expect(find_field('Sound').find('option[selected]').value).to eq 'bike' end + step 'I click jira service link' do + click_link 'JIRA' + end + + step 'I fill jira settings' do + fill_in 'Project url', with: 'http://jira.example' + fill_in 'Username', with: 'gitlab' + fill_in 'Password', with: 'gitlab' + fill_in 'Api url', with: 'http://jira.example/rest/api/2' + click_button 'Save' + end + + step 'I should see jira service settings saved' do + expect(find_field('Project url').value).to eq 'http://jira.example' + expect(find_field('Username').value).to eq 'gitlab' + expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2' + end + step 'I click Atlassian Bamboo CI service link' do click_link 'Atlassian Bamboo CI' end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index a3aef9bf8c30c417093bb70d34489fd6b05a93fd..504654f90ddd12a28980281778bcae256015bd23 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -42,7 +42,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "Edit"' do - page.within ".page-title" do + page.within ".detail-page-header" do click_link "Edit" end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index b88709620ab82f7e90c1cd21f5c32eed629236aa..d08935aa1010a9f10be706ba03f8e87edba1d53d 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -5,6 +5,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include SharedPaths include RepoHelpers + step "I don't have write access" do + @project = create(:project, name: "Other Project", path: "other-project") + @project.team << [@user, :reporter] + visit namespace_project_tree_path(@project.namespace, @project, root_ref) + end + step 'I should see files from repository' do expect(page).to have_content "VERSION" expect(page).to have_content ".gitignore" @@ -75,7 +81,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new branch name' do - fill_in :new_branch, with: 'new_branch_name', visible: true + fill_in :target_branch, with: 'new_branch_name', visible: true end step 'I fill the new file name with an illegal name' do @@ -87,7 +93,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the commit message' do - fill_in :commit_message, with: 'Not yet a commit message.', visible: true + fill_in :commit_message, with: 'New commit message', visible: true end step 'I click link "Diff"' do @@ -103,7 +109,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click on "Delete"' do - click_button 'Delete' + click_on 'Delete' end step 'I click on "Delete file"' do @@ -111,7 +117,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click on "Replace"' do - click_button "Replace" + click_on "Replace" end step 'I click on "Replace file"' do @@ -124,7 +130,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I click on "New file" link in repo' do find('.add-to-tree').click - click_link 'Create file' + click_link 'New file' end step 'I click on "Upload file" link in repo' do @@ -155,7 +161,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see the new commit message' do - expect(page).to have_content "New upload commit message" + expect(page).to have_content "New commit message" end step 'I upload a new text file' do @@ -164,7 +170,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I fill the upload file commit message' do page.within('#modal-upload-blob') do - fill_in :commit_message, with: 'New upload commit message' + fill_in :commit_message, with: 'New commit message' end end @@ -238,22 +244,27 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the new file' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, 'master/' + new_file_name)) + expect(current_path).to eq( + namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name)) end step 'I am redirected to the new file with directory' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, 'master/' + new_file_name_with_directory)) + expect(current_path).to eq( + namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name_with_directory)) end step 'I am redirected to the new merge request page' do expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project)) end + step "I am redirected to the fork's new merge request page" do + fork = @user.fork_of(@project) + expect(current_path).to eq(new_namespace_project_merge_request_path(fork.namespace, fork)) + end + step 'I am redirected to the root directory' do - expect(current_path).to eq(namespace_project_tree_path( - @project.namespace, @project, 'master/')) + expect(current_path).to eq( + namespace_project_tree_path(@project.namespace, @project, 'master')) end step "I don't see the permalink link" do @@ -332,8 +343,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content 'Permalink' expect(page).not_to have_content 'Edit' expect(page).not_to have_content 'Blame' - expect(page).not_to have_content 'Delete' - expect(page).not_to have_content 'Replace' + expect(page).to have_content 'Delete' + expect(page).to have_content 'Replace' + end + + step 'I should see a notice about a new fork having been created' do + expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." end private diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index bd2e0619cddd8be94d6717fec3d268a41313cc14..9f7c748a3b78ff3bae147189db11af8c430908e2 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -32,6 +32,6 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps protected def has_n_stars(n) - expect(page).to have_css(".star-btn .count", text: n, visible: true) + expect(page).to have_css(".star-count", text: n, visible: true) end end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index dd466cde28d0efb5fa6a2c0bfab6cdea68053fee..c6a0ae2ba38e06110bce21311a248aaccd74d27c 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -166,7 +166,7 @@ module SharedDiffNote end step 'I should see add a diff comment button' do - expect(page).to have_css('.js-add-diff-note-button', visible: true) + expect(page).to have_css('.js-add-diff-note-button') end step 'I should see an empty diff comment form' do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index c74a5fd3bc7e7cfa7d044ccb3182622847b0baa6..b33bd332655849909229518a722539a9323cc23c 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -212,8 +212,8 @@ module SharedPaths end step 'I visit a binary file in the repo' do - visit namespace_project_blob_path(@project.namespace, @project, File.join( - root_ref, 'files/images/logo-black.png')) + visit namespace_project_blob_path(@project.namespace, @project, + File.join(root_ref, 'files/images/logo-black.png')) end step "I visit my project's commits page" do @@ -316,8 +316,8 @@ module SharedPaths end step 'I am on the ".gitignore" edit file page' do - expect(current_path).to eq(namespace_project_edit_blob_path( - @project.namespace, @project, File.join(root_ref, '.gitignore'))) + expect(current_path).to eq( + namespace_project_edit_blob_path(@project.namespace, @project, File.join(root_ref, '.gitignore'))) end step 'I visit project source page for "6d39438"' do diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 80d1ddeef055177d7df0d3ef3e2e46843e2a3609..023032e679f219001de31470cb25f97410b823a9 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -13,7 +13,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps end step 'I click link "Edit"' do - page.within ".page-title" do + page.within ".detail-page-header" do click_link "Edit" end end diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json new file mode 100644 index 0000000000000000000000000000000000000000..547ce7978b389ca4a5273cdbdbbcabb9bb42d275 --- /dev/null +++ b/fixtures/emojis/aliases.json @@ -0,0 +1,367 @@ +{ + "northeast_pointing_airplane":"airplane_northeast", + "small_airplane":"airplane_small", + "up_pointing_small_airplane":"airplane_small_up", + "up_pointing_airplane":"airplane_up", + "left_anger_bubble":"anger_left", + "right_anger_bubble":"anger_right", + "ballot_box_with_ballot":"ballot_box", + "ballot_box_with_bold_check":"ballot_box_check", + "ballot_box_with_script_x":"ballot_box_x", + "ballot_script_x":"ballot_x", + "beach_with_umbrella":"beach", + "bellhop_bell":"bellhop", + "bouquet_of_flowers":"bouquet2", + "bullhorn_with_sound_waves":"bullhorn_waves", + "pocket calculator":"calculator", + "spiral_calendar_pad":"calendar_spiral", + "card_file_box":"card_box", + "tape_cartridge":"cartridge", + "city_sunrise":"city_sunset", + "mantlepiece_clock":"clock", + "clockwise_right_and_left_semicircle_arrows":"clockwise_arrows", + "cloud_with_lightning":"cloud_lightning", + "cloud_with_rain":"cloud_rain", + "cloud_with_snow":"cloud_snow", + "cloud_with_tornado":"cloud_tornado", + "old_personal_computer":"computer_old", + "building_construction":"contruction_site", + "couch_and_lamp":"couch", + "couple_with_heart_mm":"couple_mm", + "couple_with_heart_ww":"couple_ww", + "lower_left_crayon":"crayon", + "heavy_latin_cross":"cross_heavy", + "white_latin_cross":"cross_white", + "black_skull_and_crossbones":"crossbones", + "passenger_ship":"cruise_ship", + "dagger_knife":"dagger", + "desktop_computer":"desktop", + "card_index_dividers":"dividers", + "document_with_text":"document_text", + "dove_of_peace":"dove", + "email":"e-mail", + "back_of_envelope":"envelope_back", + "flying_envelope":"envelope_flying", + "stamped_envelope":"envelope_stamped", + "pen_over_stamped_envelope":"envelope_stamped_pen", + "white_down_pointing_left_hand_index":"finger_pointing_down", + "sideways_white_down_pointing_index":"finger_pointing_down2", + "sideways_white_left_pointing_index":"finger_pointing_left", + "sideways_white_right_pointing_index":"finger_pointing_right", + "sideways_white_up_pointing_index":"finger_pointing_up", + "flame":"fire", + "oncoming_fire_engine":"fire_engine_oncoming", + "ac":"flag_ac", + "ad":"flag_ad", + "ae":"flag_ae", + "af":"flag_af", + "ag":"flag_ag", + "ai":"flag_ai", + "al":"flag_al", + "am":"flag_am", + "ao":"flag_ao", + "ar":"flag_ar", + "at":"flag_at", + "au":"flag_au", + "aw":"flag_aw", + "az":"flag_az", + "ba":"flag_ba", + "bb":"flag_bb", + "bd":"flag_bd", + "be":"flag_be", + "bf":"flag_bf", + "bg":"flag_bg", + "bh":"flag_bh", + "bi":"flag_bi", + "bj":"flag_bj", + "waving_black_flag":"flag_black", + "bm":"flag_bm", + "bn":"flag_bn", + "bo":"flag_bo", + "br":"flag_br", + "bs":"flag_bs", + "bt":"flag_bt", + "bw":"flag_bw", + "by":"flag_by", + "bz":"flag_bz", + "ca":"flag_ca", + "congo":"flag_cd", + "cf":"flag_cf", + "cg":"flag_cg", + "ch":"flag_ch", + "ci":"flag_ci", + "chile":"flag_cl", + "cm":"flag_cm", + "cn":"flag_cn", + "co":"flag_co", + "cr":"flag_cr", + "cu":"flag_cu", + "cv":"flag_cv", + "cy":"flag_cy", + "cz":"flag_cz", + "de":"flag_de", + "dj":"flag_dj", + "dk":"flag_dk", + "dm":"flag_dm", + "do":"flag_do", + "dz":"flag_dz", + "ec":"flag_ec", + "ee":"flag_ee", + "eg":"flag_eg", + "eh":"flag_eh", + "er":"flag_er", + "es":"flag_es", + "et":"flag_et", + "fi":"flag_fi", + "fj":"flag_fj", + "fk":"flag_fk", + "fm":"flag_fm", + "fo":"flag_fo", + "fr":"flag_fr", + "ga":"flag_ga", + "gb":"flag_gb", + "gd":"flag_gd", + "ge":"flag_ge", + "gh":"flag_gh", + "gi":"flag_gi", + "gl":"flag_gl", + "gm":"flag_gm", + "gn":"flag_gn", + "gq":"flag_gq", + "gr":"flag_gr", + "gt":"flag_gt", + "gu":"flag_gu", + "gw":"flag_gw", + "gy":"flag_gy", + "hk":"flag_hk", + "hn":"flag_hn", + "hr":"flag_hr", + "ht":"flag_ht", + "hu":"flag_hu", + "indonesia":"flag_id", + "ie":"flag_ie", + "il":"flag_il", + "in":"flag_in", + "iq":"flag_iq", + "ir":"flag_ir", + "is":"flag_is", + "it":"flag_it", + "je":"flag_je", + "jm":"flag_jm", + "jo":"flag_jo", + "jp":"flag_jp", + "ke":"flag_ke", + "kg":"flag_kg", + "kh":"flag_kh", + "ki":"flag_ki", + "km":"flag_km", + "kn":"flag_kn", + "kp":"flag_kp", + "kr":"flag_kr", + "kw":"flag_kw", + "ky":"flag_ky", + "kz":"flag_kz", + "la":"flag_la", + "lb":"flag_lb", + "lc":"flag_lc", + "li":"flag_li", + "lk":"flag_lk", + "lr":"flag_lr", + "ls":"flag_ls", + "lt":"flag_lt", + "lu":"flag_lu", + "lv":"flag_lv", + "ly":"flag_ly", + "ma":"flag_ma", + "mc":"flag_mc", + "md":"flag_md", + "me":"flag_me", + "mg":"flag_mg", + "mh":"flag_mh", + "mk":"flag_mk", + "ml":"flag_ml", + "mm":"flag_mm", + "mn":"flag_mn", + "mo":"flag_mo", + "mr":"flag_mr", + "ms":"flag_ms", + "mt":"flag_mt", + "mu":"flag_mu", + "mv":"flag_mv", + "mw":"flag_mw", + "mx":"flag_mx", + "my":"flag_my", + "mz":"flag_mz", + "na":"flag_na", + "nc":"flag_nc", + "ne":"flag_ne", + "nigeria":"flag_ng", + "ni":"flag_ni", + "nl":"flag_nl", + "no":"flag_no", + "np":"flag_np", + "nr":"flag_nr", + "nu":"flag_nu", + "nz":"flag_nz", + "om":"flag_om", + "pa":"flag_pa", + "pe":"flag_pe", + "pf":"flag_pf", + "pg":"flag_pg", + "ph":"flag_ph", + "pk":"flag_pk", + "pl":"flag_pl", + "pr":"flag_pr", + "ps":"flag_ps", + "pt":"flag_pt", + "pw":"flag_pw", + "py":"flag_py", + "qa":"flag_qa", + "ro":"flag_ro", + "rs":"flag_rs", + "ru":"flag_ru", + "rw":"flag_rw", + "saudiarabia":"flag_sa", + "saudi":"flag_sa", + "sb":"flag_sb", + "sc":"flag_sc", + "sd":"flag_sd", + "se":"flag_se", + "sg":"flag_sg", + "sh":"flag_sh", + "si":"flag_si", + "sk":"flag_sk", + "sl":"flag_sl", + "sm":"flag_sm", + "sn":"flag_sn", + "so":"flag_so", + "sr":"flag_sr", + "st":"flag_st", + "sv":"flag_sv", + "sy":"flag_sy", + "sz":"flag_sz", + "td":"flag_td", + "tg":"flag_tg", + "th":"flag_th", + "tj":"flag_tj", + "tl":"flag_tl", + "turkmenistan":"flag_tm", + "tn":"flag_tn", + "to":"flag_to", + "tr":"flag_tr", + "tt":"flag_tt", + "tuvalu":"flag_tv", + "tw":"flag_tw", + "tz":"flag_tz", + "ua":"flag_ua", + "ug":"flag_ug", + "us":"flag_us", + "uy":"flag_uy", + "uz":"flag_uz", + "va":"flag_va", + "vc":"flag_vc", + "ve":"flag_ve", + "vi":"flag_vi", + "vn":"flag_vn", + "vu":"flag_vu", + "wf":"flag_wf", + "waving_white_flag":"flag_white", + "ws":"flag_ws", + "xk":"flag_xk", + "ye":"flag_ye", + "za":"flag_za", + "zm":"flag_zm", + "zw":"flag_zw", + "clamshell_mobile_phone":"flip_phone", + "black_hard_shell_floppy_disk":"floppy_black", + "white_hard_shell_floppy_disk":"floppy_white", + "open_folder":"folder_open", + "fork_and_knife_with_plate":"fork_knife_plate", + "frame_with_picture":"frame_photo", + "frame_with_tiles":"frame_tiles", + "frame_with_an_x":"frame_x", + "anguished":"frowning", + "raised_hand_with_fingers_splayed":"hand_splayed", + "reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse", + "reversed_victory_hand":"hand_victory", + "heart_with_tip_on_the_left":"heart_tip", + "house_buildings":"homes", + "derelict_house_building":"house_abandoned", + "circled_information_source":"info", + "desert_island":"island", + "up_pointing_military_airplane":"jet_up", + "old_key":"key2", + "wired_keyboard":"keyboard", + "keyboard_and_mouse":"keyboard_mouse", + "musical_keyboard_with_jacks":"keyboard_with_jacks", + "couplekiss_mm":"kiss_mm", + "couplekiss_ww":"kiss_ww", + "satisfied":"laughing", + "left_hand_telephone_receiver":"left_receiver", + "man_in_business_suit_levitating":"levitate", + "weight_lifter":"lifter", + "light_mark":"light_check_mark", + "world_map":"map", + "sports_medal":"medal", + "studio_microphone":"microphone2", + "reversed_hand_with_middle_finger_extended":"middle_finger", + "lightning_mood_bubble":"mood_bubble_lightning", + "lightning_mood":"mood_lightning", + "racing_motorcycle":"motorcycle", + "snow_capped_mountain":"mountain_snow", + "one_button_mouse":"mouse_one", + "three_networked_computers":"network", + "rolled_up_newspaper":"newspaper2", + "note_page":"note", + "empty_note_page":"note_empty", + "note_pad":"notepad", + "empty_note_pad":"notepad_empty", + "spiral_note_pad":"notepad_spiral", + "oil_drum":"oil", + "grandma":"older_woman", + "optical_disc_icon":"optical_disk", + "lower_left_paintbrush":"paintbrush", + "linked_paperclips":"paperclips", + "national_park":"park", + "lower_left_ballpoint_pen":"pen_ballpoint", + "lower_left_fountain_pen":"pen_fountain", + "memo":"pencil", + "lower_left_pencil":"pencil3", + "black_pennant":"pennant_black", + "white_pennant":"pennant_white", + "no_piracy":"piracy", + "shit":"poop", + "hankey":"poop", + "poo":"poop", + "prohibited_sign":"prohibited", + "film_projector":"projector", + "racing_car":"race_car", + "railroad_track":"railway_track", + "right_speaker_with_one_sound_wave":"right_speaker_one", + "right_speaker_with_three_sound_waves":"right_speaker_three", + "skeleton":"skull", + "slightly_frowning_face":"slight_frown", + "slightly_smiling_face":"slight_smile", + "speaking_head_in_silhouette":"speaking_head", + "left_speech_bubble":"speech_left", + "right_speech_bubble":"speech_right", + "three_speech_bubbles":"speech_three", + "two_speech_bubbles":"speech_two", + "sleuth_or_spy":"spy", + "portable_stereo":"stereo", + "black_touchtone_telephone":"telephone_black", + "white_touchtone_telephone":"telephone_white", + "left_thought_bubble":"thought_left", + "right_thought_bubble":"thought_right", + "reversed_thumbs_down_sign":"thumbs_down_reverse", + "reversed_thumbs_up_sign":"thumbs_up_reverse", + "-1":"thumbsdown", + "+1":"thumbsup", + "admission_tickets":"tickets", + "hammer_and_wrench":"tools", + "diesel_locomotive":"train_diesel", + "triangle_with_rounded_corners":"triangle_round", + "turned_ok_hand_sign":"turned_ok_hand", + "raised_hand_with_part_between_middle_and_ring_fingers":"vulcan", + "left_writing_hand":"writing_hand" +} \ No newline at end of file diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json new file mode 100644 index 0000000000000000000000000000000000000000..60ef2399e14de6b711aa4a5e0b92ce97ab1e791a --- /dev/null +++ b/fixtures/emojis/index.json @@ -0,0 +1,13376 @@ +{ + "100": { + "unicode": "1F4AF", + "unicode_alternates": [], + "name": "hundred points symbol", + "shortname": ":100:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["numbers", "perfect", "score", "100", "percent", "a", "plus", "perfect", "school", "quiz", "score", "test", "exam"], + "moji": "💯" + }, + "1234": { + "unicode": "1F522", + "unicode_alternates": [], + "name": "input symbol for numbers", + "shortname": ":1234:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "numbers"], + "moji": "🔢" + }, + "8ball": { + "unicode": "1F3B1", + "unicode_alternates": [], + "name": "billiards", + "shortname": ":8ball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["pool", "billiards", "eight ball", "pool", "pocket ball", "cue"], + "moji": "🎱" + }, + "a": { + "unicode": "1F170", + "unicode_alternates": [], + "name": "negative squared latin capital letter a", + "shortname": ":a:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "letter", "red-square"], + "moji": "🅰" + }, + "ab": { + "unicode": "1F18E", + "unicode_alternates": [], + "name": "negative squared ab", + "shortname": ":ab:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "red-square"], + "moji": "🆎" + }, + "abc": { + "unicode": "1F524", + "unicode_alternates": [], + "name": "input symbol for latin letters", + "shortname": ":abc:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-square"], + "moji": "🔤" + }, + "abcd": { + "unicode": "1F521", + "unicode_alternates": [], + "name": "input symbol for latin small letters", + "shortname": ":abcd:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-square"], + "moji": "🔡" + }, + "accept": { + "unicode": "1F251", + "unicode_alternates": [], + "name": "circled ideograph accept", + "shortname": ":accept:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["agree", "chinese", "good", "kanji", "ok", "yes"], + "moji": "🉑" + }, + "aerial_tramway": { + "unicode": "1F6A1", + "unicode_alternates": [], + "name": "aerial tramway", + "shortname": ":aerial_tramway:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "aerial", "tram", "tramway", "cable", "transport"], + "moji": "🚡" + }, + "airplane": { + "unicode": "2708", + "unicode_alternates": ["2708-FE0F"], + "name": "airplane", + "shortname": ":airplane:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flight", "transportation", "vehicle", "airplane", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"], + "moji": "✈" + }, + "airplane_arriving": { + "unicode": "1F6EC", + "unicode_alternates": [], + "name": "airplane arriving", + "shortname": ":airplane_arriving:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"] + }, + "airplane_departure": { + "unicode": "1F6EB", + "unicode_alternates": [], + "name": "airplane departure", + "shortname": ":airplane_departure:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus", "leaving"] + }, + "airplane_northeast": { + "unicode": "1F6EA", + "unicode_alternates": [], + "name": "northeast-pointing airplane", + "shortname": ":airplane_northeast:", + "category": "travel_places", + "aliases": [":northeast_pointing_airplane:"], + "aliases_ascii": [], + "keywords": ["plane", "travel"] + }, + "airplane_small": { + "unicode": "1F6E9", + "unicode_alternates": [], + "name": "small airplane", + "shortname": ":airplane_small:", + "category": "travel_places", + "aliases": [":small_airplane:"], + "aliases_ascii": [], + "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"] + }, + "airplane_small_up": { + "unicode": "1F6E8", + "unicode_alternates": [], + "name": "up-pointing small airplane", + "shortname": ":airplane_small_up:", + "category": "travel_places", + "aliases": [":up_pointing_small_airplane:"], + "aliases_ascii": [], + "keywords": ["plane", "travel"] + }, + "airplane_up": { + "unicode": "1F6E7", + "unicode_alternates": [], + "name": "up-pointing airplane", + "shortname": ":airplane_up:", + "category": "travel_places", + "aliases": [":up_pointing_airplane:"], + "aliases_ascii": [], + "keywords": ["plane", "travel"] + }, + "alarm_clock": { + "unicode": "23F0", + "unicode_alternates": [], + "name": "alarm clock", + "shortname": ":alarm_clock:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["time", "wake"], + "moji": "â°" + }, + "alien": { + "unicode": "1F47D", + "unicode_alternates": [], + "name": "extraterrestrial alien", + "shortname": ":alien:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["UFO", "paul", "alien", "ufo"], + "moji": "👽" + }, + "ambulance": { + "unicode": "1F691", + "unicode_alternates": [], + "name": "ambulance", + "shortname": ":ambulance:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["911", "health", "ambulance", "emergency", "medical", "help", "assistance"], + "moji": "🚑" + }, + "anchor": { + "unicode": "2693", + "unicode_alternates": ["2693-FE0F"], + "name": "anchor", + "shortname": ":anchor:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ferry", "ship", "anchor", "ship", "boat", "ocean", "harbor", "marina", "shipyard", "sailor", "tattoo"], + "moji": "âš“" + }, + "angel": { + "unicode": "1F47C", + "unicode_alternates": [], + "name": "baby angel", + "shortname": ":angel:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["baby", "angel", "halo", "cupid", "wings", "halo", "heaven", "wings", "jesus"], + "moji": "👼" + }, + "anger": { + "unicode": "1F4A2", + "unicode_alternates": [], + "name": "anger symbol", + "shortname": ":anger:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["anger", "angry", "mad"], + "moji": "💢" + }, + "anger_left": { + "unicode": "1F5EE", + "unicode_alternates": [], + "name": "left anger bubble", + "shortname": ":anger_left:", + "category": "objects_symbols", + "aliases": [":left_anger_bubble:"], + "aliases_ascii": [], + "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"] + }, + "anger_right": { + "unicode": "1F5EF", + "unicode_alternates": [], + "name": "right anger bubble", + "shortname": ":anger_right:", + "category": "objects_symbols", + "aliases": [":right_anger_bubble:"], + "aliases_ascii": [], + "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"] + }, + "angry": { + "unicode": "1F620", + "unicode_alternates": [], + "name": "angry face", + "shortname": ":angry:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [">:(", ">:-(", ":@"], + "keywords": ["angry", "livid", "mad", "vexed", "irritated", "annoyed", "face", "frustrated", "mad"], + "moji": "😠" + }, + "anguished": { + "unicode": "1F627", + "unicode_alternates": [], + "name": "anguished face", + "shortname": ":anguished:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "nervous", "stunned", "pain", "anguish", "ouch", "misery", "distress", "grief"], + "moji": "😧" + }, + "ant": { + "unicode": "1F41C", + "unicode_alternates": [], + "name": "ant", + "shortname": ":ant:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "insect", "ant", "queen", "insect", "team"], + "moji": "ðŸœ" + }, + "apple": { + "unicode": "1F34E", + "unicode_alternates": [], + "name": "red apple", + "shortname": ":apple:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fruit", "mac", "apple", "fruit", "electronics", "red", "doctor", "teacher", "school", "core"], + "moji": "ðŸŽ" + }, + "aquarius": { + "unicode": "2652", + "unicode_alternates": ["2652-FE0F"], + "name": "aquarius", + "shortname": ":aquarius:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["aquarius", "water", "bearer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "moji": "â™’" + }, + "aries": { + "unicode": "2648", + "unicode_alternates": ["2648-FE0F"], + "name": "aries", + "shortname": ":aries:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["aries", "ram", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "moji": "♈" + }, + "arrow_backward": { + "unicode": "25C0", + "unicode_alternates": ["25C0-FE0F"], + "name": "black left-pointing triangle", + "shortname": ":arrow_backward:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "â—€" + }, + "arrow_double_down": { + "unicode": "23EC", + "unicode_alternates": [], + "name": "black down-pointing double triangle", + "shortname": ":arrow_double_down:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "â¬" + }, + "arrow_double_up": { + "unicode": "23EB", + "unicode_alternates": [], + "name": "black up-pointing double triangle", + "shortname": ":arrow_double_up:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "â«" + }, + "arrow_down": { + "unicode": "2B07", + "unicode_alternates": ["2B07-FE0F"], + "name": "downwards black arrow", + "shortname": ":arrow_down:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "⬇" + }, + "arrow_down_small": { + "unicode": "1F53D", + "unicode_alternates": [], + "name": "down-pointing small red triangle", + "shortname": ":arrow_down_small:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "🔽" + }, + "arrow_forward": { + "unicode": "25B6", + "unicode_alternates": ["25B6-FE0F"], + "name": "black right-pointing triangle", + "shortname": ":arrow_forward:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "â–¶" + }, + "arrow_heading_down": { + "unicode": "2935", + "unicode_alternates": ["2935-FE0F"], + "name": "arrow pointing rightwards then curving downwards", + "shortname": ":arrow_heading_down:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "⤵" + }, + "arrow_heading_up": { + "unicode": "2934", + "unicode_alternates": ["2934-FE0F"], + "name": "arrow pointing rightwards then curving upwards", + "shortname": ":arrow_heading_up:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "⤴" + }, + "arrow_left": { + "unicode": "2B05", + "unicode_alternates": ["2B05-FE0F"], + "name": "leftwards black arrow", + "shortname": ":arrow_left:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square", "previous"], + "moji": "⬅" + }, + "arrow_lower_left": { + "unicode": "2199", + "unicode_alternates": ["2199-FE0F"], + "name": "south west arrow", + "shortname": ":arrow_lower_left:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "↙" + }, + "arrow_lower_right": { + "unicode": "2198", + "unicode_alternates": ["2198-FE0F"], + "name": "south east arrow", + "shortname": ":arrow_lower_right:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "blue-square"], + "moji": "↘" + }, + "arrow_right": { + "unicode": "27A1", + "unicode_alternates": ["27A1-FE0F"], + "name": "black rightwards arrow", + "shortname": ":arrow_right:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "next"], + "moji": "âž¡" + }, + "arrow_right_hook": { + "unicode": "21AA", + "unicode_alternates": ["21AA-FE0F"], + "name": "rightwards arrow with hook", + "shortname": ":arrow_right_hook:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "↪" + }, + "arrow_up": { + "unicode": "2B06", + "unicode_alternates": ["2B06-FE0F"], + "name": "upwards black arrow", + "shortname": ":arrow_up:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "⬆" + }, + "arrow_up_down": { + "unicode": "2195", + "unicode_alternates": ["2195-FE0F"], + "name": "up down arrow", + "shortname": ":arrow_up_down:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "↕" + }, + "arrow_up_small": { + "unicode": "1F53C", + "unicode_alternates": [], + "name": "up-pointing small red triangle", + "shortname": ":arrow_up_small:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "🔼" + }, + "arrow_upper_left": { + "unicode": "2196", + "unicode_alternates": ["2196-FE0F"], + "name": "north west arrow", + "shortname": ":arrow_upper_left:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "↖" + }, + "arrow_upper_right": { + "unicode": "2197", + "unicode_alternates": ["2197-FE0F"], + "name": "north east arrow", + "shortname": ":arrow_upper_right:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "↗" + }, + "arrows_clockwise": { + "unicode": "1F503", + "unicode_alternates": [], + "name": "clockwise downwards and upwards open circle arrows", + "shortname": ":arrows_clockwise:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sync"], + "moji": "🔃" + }, + "arrows_counterclockwise": { + "unicode": "1F504", + "unicode_alternates": [], + "name": "anticlockwise downwards and upwards open circle ar", + "shortname": ":arrows_counterclockwise:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "sync"], + "moji": "🔄" + }, + "art": { + "unicode": "1F3A8", + "unicode_alternates": [], + "name": "artist palette", + "shortname": ":art:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["design", "draw", "paint", "artist", "palette", "art", "colors", "paint", "draw", "brush", "pastels", "oils"], + "moji": "🎨" + }, + "articulated_lorry": { + "unicode": "1F69B", + "unicode_alternates": [], + "name": "articulated lorry", + "shortname": ":articulated_lorry:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "transportation", "vehicle", "truck", "delivery", "semi", "lorry", "articulated"], + "moji": "🚛" + }, + "ascending_notes": { + "unicode": "1F39C", + "unicode_alternates": [], + "name": "beamed ascending musical notes", + "shortname": ":ascending_notes:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["score", "music", "sound", "tone"] + }, + "astonished": { + "unicode": "1F632", + "unicode_alternates": [], + "name": "astonished face", + "shortname": ":astonished:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "xox", "shocked", "surprise", "astonished"], + "moji": "😲" + }, + "athletic_shoe": { + "unicode": "1F45F", + "unicode_alternates": [], + "name": "athletic shoe", + "shortname": ":athletic_shoe:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shoes", "sports"], + "moji": "👟" + }, + "atm": { + "unicode": "1F3E7", + "unicode_alternates": [], + "name": "automated teller machine", + "shortname": ":atm:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["atm", "cash", "withdrawal", "money", "deposit", "financial", "bank", "adam", "payday", "bank", "blue-square", "cash", "money", "payment"], + "moji": "ðŸ§" + }, + "b": { + "unicode": "1F171", + "unicode_alternates": [], + "name": "negative squared latin capital letter b", + "shortname": ":b:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "letter", "red-square"], + "moji": "🅱" + }, + "baby": { + "unicode": "1F476", + "unicode_alternates": [], + "name": "baby", + "shortname": ":baby:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["boy", "child", "infant"], + "moji": "👶" + }, + "baby_bottle": { + "unicode": "1F37C", + "unicode_alternates": [], + "name": "baby bottle", + "shortname": ":baby_bottle:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["container", "food", "baby", "bottle", "milk", "mother", "nipple", "newborn", "formula"], + "moji": "ðŸ¼" + }, + "baby_chick": { + "unicode": "1F424", + "unicode_alternates": [], + "name": "baby chick", + "shortname": ":baby_chick:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"], + "moji": "ðŸ¤" + }, + "baby_symbol": { + "unicode": "1F6BC", + "unicode_alternates": [], + "name": "baby symbol", + "shortname": ":baby_symbol:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["child", "orange-square", "baby", "crawl", "newborn", "human", "diaper", "small", "babe"], + "moji": "🚼" + }, + "back": { + "unicode": "1F519", + "unicode_alternates": [], + "name": "back with leftwards arrow above", + "shortname": ":back:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow"], + "moji": "🔙" + }, + "baggage_claim": { + "unicode": "1F6C4", + "unicode_alternates": [], + "name": "baggage claim", + "shortname": ":baggage_claim:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["airport", "blue-square", "transport", "bag", "baggage", "luggage", "travel"], + "moji": "🛄" + }, + "balloon": { + "unicode": "1F388", + "unicode_alternates": [], + "name": "balloon", + "shortname": ":balloon:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["celebration", "party", "balloon", "birthday", "celebration", "helium", "gas", "children", "float"], + "moji": "🎈" + }, + "ballot_box": { + "unicode": "1F5F3", + "unicode_alternates": [], + "name": "ballot box with ballot", + "shortname": ":ballot_box:", + "category": "objects_symbols", + "aliases": [":ballot_box_with_ballot:"], + "aliases_ascii": [], + "keywords": ["vote"] + }, + "ballot_box_check": { + "unicode": "1F5F9", + "unicode_alternates": [], + "name": "ballot box with bold check", + "shortname": ":ballot_box_check:", + "category": "objects_symbols", + "aliases": [":ballot_box_with_bold_check:"], + "aliases_ascii": [], + "keywords": ["mark", "vote"] + }, + "ballot_box_with_check": { + "unicode": "2611", + "unicode_alternates": ["2611-FE0F"], + "name": "ballot box with check", + "shortname": ":ballot_box_with_check:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["agree", "ok"], + "moji": "☑" + }, + "ballot_box_x": { + "unicode": "1F5F5", + "unicode_alternates": [], + "name": "ballot box with script x", + "shortname": ":ballot_box_x:", + "category": "objects_symbols", + "aliases": [":ballot_box_with_script_x:"], + "aliases_ascii": [], + "keywords": ["mark", "vote"] + }, + "ballot_x": { + "unicode": "1F5F4", + "unicode_alternates": [], + "name": "ballot script x", + "shortname": ":ballot_x:", + "category": "objects_symbols", + "aliases": [":ballot_script_x:"], + "aliases_ascii": [], + "keywords": ["mark", "vote"] + }, + "bamboo": { + "unicode": "1F38D", + "unicode_alternates": [], + "name": "pine decoration", + "shortname": ":bamboo:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "vegetable", "pine", "bamboo", "decoration", "new", "years", "spirits", "harvest", "prosperity", "longevity", "fortune", "luck", "welcome", "farming", "agriculture"], + "moji": "ðŸŽ" + }, + "banana": { + "unicode": "1F34C", + "unicode_alternates": [], + "name": "banana", + "shortname": ":banana:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "banana", "peel", "bunch"], + "moji": "ðŸŒ" + }, + "bangbang": { + "unicode": "203C", + "unicode_alternates": ["203C-FE0F"], + "name": "double exclamation mark", + "shortname": ":bangbang:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["exclamation", "surprise"], + "moji": "‼" + }, + "bank": { + "unicode": "1F3E6", + "unicode_alternates": [], + "name": "bank", + "shortname": ":bank:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building"], + "moji": "ðŸ¦" + }, + "bar_chart": { + "unicode": "1F4CA", + "unicode_alternates": [], + "name": "bar chart", + "shortname": ":bar_chart:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph", "presentation", "stats"], + "moji": "📊" + }, + "barber": { + "unicode": "1F488", + "unicode_alternates": [], + "name": "barber pole", + "shortname": ":barber:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hair", "salon", "style"], + "moji": "💈" + }, + "baseball": { + "unicode": "26BE", + "unicode_alternates": ["26BE-FE0F"], + "name": "baseball", + "shortname": ":baseball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["MLB", "balls", "sports"], + "moji": "âš¾" + }, + "basketball": { + "unicode": "1F3C0", + "unicode_alternates": [], + "name": "basketball and hoop", + "shortname": ":basketball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["NBA", "balls", "sports", "basketball", "bball", "dribble", "hoop", "net", "swish", "rip city"], + "moji": "ðŸ€" + }, + "bath": { + "unicode": "1F6C0", + "unicode_alternates": [], + "name": "bath", + "shortname": ":bath:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"], + "moji": "🛀" + }, + "bathtub": { + "unicode": "1F6C1", + "unicode_alternates": [], + "name": "bathtub", + "shortname": ":bathtub:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"], + "moji": "ðŸ›" + }, + "battery": { + "unicode": "1F50B", + "unicode_alternates": [], + "name": "battery", + "shortname": ":battery:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["energy", "power", "sustain"], + "moji": "🔋" + }, + "beach": { + "unicode": "1F3D6", + "unicode_alternates": [], + "name": "beach with umbrella", + "shortname": ":beach:", + "category": "travel_places", + "aliases": [":beach_with_umbrella:"], + "aliases_ascii": [], + "keywords": ["sand", "sun", "surf", "vacation", "relaxation", "tanning", "tan", "swimming"] + }, + "bear": { + "unicode": "1F43B", + "unicode_alternates": [], + "name": "bear face", + "shortname": ":bear:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ»" + }, + "bed": { + "unicode": "1F6CF", + "unicode_alternates": [], + "name": "bed", + "shortname": ":bed:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sleep", "sex", "queen", "full", "twin", "king", "mattress"] + }, + "bee": { + "unicode": "1F41D", + "unicode_alternates": [], + "name": "honeybee", + "shortname": ":bee:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "insect", "bee", "queen", "buzz", "flower", "pollen", "sting", "honey", "hive", "bumble", "pollination"], + "moji": "ðŸ" + }, + "beer": { + "unicode": "1F37A", + "unicode_alternates": [], + "name": "beer mug", + "shortname": ":beer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "hops", "mug", "barley", "malt", "yeast", "portland", "oregon", "brewery", "micro", "pint", "boot"], + "moji": "ðŸº" + }, + "beers": { + "unicode": "1F37B", + "unicode_alternates": [], + "name": "clinking beer mugs", + "shortname": ":beers:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "beers", "cheers", "mug", "toast", "celebrate", "pub", "bar", "jolly", "hops", "clink"], + "moji": "ðŸ»" + }, + "beetle": { + "unicode": "1F41E", + "unicode_alternates": [], + "name": "lady beetle", + "shortname": ":beetle:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["insect", "nature", "lady", "bug", "ladybug", "ladybird", "beetle", "cow", "lady cow", "insect", "endearment"], + "moji": "ðŸž" + }, + "beginner": { + "unicode": "1F530", + "unicode_alternates": [], + "name": "japanese symbol for beginner", + "shortname": ":beginner:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["badge", "shield"], + "moji": "🔰" + }, + "bell": { + "unicode": "1F514", + "unicode_alternates": [], + "name": "bell", + "shortname": ":bell:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chime", "christmas", "notification", "sound", "xmas"], + "moji": "🔔" + }, + "bellhop": { + "unicode": "1F6CE", + "unicode_alternates": [], + "name": "bellhop bell", + "shortname": ":bellhop:", + "category": "travel_places", + "aliases": [":bellhop_bell:"], + "aliases_ascii": [], + "keywords": ["hotel", "porter", "ding"] + }, + "bento": { + "unicode": "1F371", + "unicode_alternates": [], + "name": "bento box", + "shortname": ":bento:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["box", "food", "japanese", "bento", "japanese", "rice", "meal", "box", "obento", "convenient", "lunchbox"], + "moji": "ðŸ±" + }, + "bicyclist": { + "unicode": "1F6B4", + "unicode_alternates": [], + "name": "bicyclist", + "shortname": ":bicyclist:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bike", "exercise", "hipster", "sports", "bicyclist", "road", "bike", "pedal", "bicycle", "transportation"], + "moji": "🚴" + }, + "bike": { + "unicode": "1F6B2", + "unicode_alternates": [], + "name": "bicycle", + "shortname": ":bike:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bicycle", "exercise", "hipster", "sports", "bike", "pedal", "bicycle", "transportation"], + "moji": "🚲" + }, + "bikini": { + "unicode": "1F459", + "unicode_alternates": [], + "name": "bikini", + "shortname": ":bikini:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beach", "fashion", "female", "girl", "swimming", "woman"], + "moji": "👙" + }, + "bird": { + "unicode": "1F426", + "unicode_alternates": [], + "name": "bird", + "shortname": ":bird:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "fly", "nature", "tweet"], + "moji": "ðŸ¦" + }, + "birthday": { + "unicode": "1F382", + "unicode_alternates": [], + "name": "birthday cake", + "shortname": ":birthday:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cake", "party", "birthday", "birth", "cake", "dessert", "wish", "celebrate"], + "moji": "🎂" + }, + "black_circle": { + "unicode": "26AB", + "unicode_alternates": ["26AB-FE0F"], + "name": "medium black circle", + "shortname": ":black_circle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "âš«" + }, + "black_joker": { + "unicode": "1F0CF", + "unicode_alternates": [], + "name": "playing card black joker", + "shortname": ":black_joker:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cards", "game", "poker"], + "moji": "ðŸƒ" + }, + "black_large_square": { + "unicode": "2B1B", + "unicode_alternates": ["2B1B-FE0F"], + "name": "black large square", + "shortname": ":black_large_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "⬛" + }, + "black_medium_small_square": { + "unicode": "25FE", + "unicode_alternates": ["25FE-FE0F"], + "name": "black medium small square", + "shortname": ":black_medium_small_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "â—¾" + }, + "black_medium_square": { + "unicode": "25FC", + "unicode_alternates": ["25FC-FE0F"], + "name": "black medium square", + "shortname": ":black_medium_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "â—¼" + }, + "black_nib": { + "unicode": "2712", + "unicode_alternates": ["2712-FE0F"], + "name": "black nib", + "shortname": ":black_nib:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["pen", "stationery"], + "moji": "✒" + }, + "black_small_square": { + "unicode": "25AA", + "unicode_alternates": ["25AA-FE0F"], + "name": "black small square", + "shortname": ":black_small_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "â–ª" + }, + "black_square_button": { + "unicode": "1F532", + "unicode_alternates": [], + "name": "black square button", + "shortname": ":black_square_button:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["frame"], + "moji": "🔲" + }, + "blossom": { + "unicode": "1F33C", + "unicode_alternates": [], + "name": "blossom", + "shortname": ":blossom:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flowers", "nature", "yellow", "blossom", "daisy", "flower"], + "moji": "🌼" + }, + "blowfish": { + "unicode": "1F421", + "unicode_alternates": [], + "name": "blowfish", + "shortname": ":blowfish:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "nature", "ocean", "sea", "blowfish", "pufferfish", "puffer", "ballonfish", "toadfish", "fugu fish", "sushi"], + "moji": "ðŸ¡" + }, + "blue_book": { + "unicode": "1F4D8", + "unicode_alternates": [], + "name": "blue book", + "shortname": ":blue_book:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["knowledge", "library", "read"], + "moji": "📘" + }, + "blue_car": { + "unicode": "1F699", + "unicode_alternates": [], + "name": "recreational vehicle", + "shortname": ":blue_car:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["car", "suv", "car", "wagon", "automobile"], + "moji": "🚙" + }, + "blue_heart": { + "unicode": "1F499", + "unicode_alternates": [], + "name": "blue heart", + "shortname": ":blue_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "blue", "heart", "love", "stability", "truth", "loyalty", "trust"], + "moji": "💙" + }, + "blush": { + "unicode": "1F60A", + "unicode_alternates": [], + "name": "smiling face with smiling eyes", + "shortname": ":blush:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["crush", "embarrassed", "face", "flushed", "happy", "shy", "smile", "smiling", "smile", "smiley"], + "moji": "😊" + }, + "boar": { + "unicode": "1F417", + "unicode_alternates": [], + "name": "boar", + "shortname": ":boar:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ—" + }, + "bomb": { + "unicode": "1F4A3", + "unicode_alternates": [], + "name": "bomb", + "shortname": ":bomb:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["boom", "explode"], + "moji": "💣" + }, + "book": { + "unicode": "1F4D6", + "unicode_alternates": [], + "name": "open book", + "shortname": ":book:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["library", "literature"], + "moji": "📖" + }, + "book2": { + "unicode": "1F56E", + "unicode_alternates": [], + "name": "book", + "shortname": ":book2:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["library", "literature", "novel", "reading", "story"] + }, + "bookmark": { + "unicode": "1F516", + "unicode_alternates": [], + "name": "bookmark", + "shortname": ":bookmark:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["favorite"], + "moji": "🔖" + }, + "bookmark_tabs": { + "unicode": "1F4D1", + "unicode_alternates": [], + "name": "bookmark tabs", + "shortname": ":bookmark_tabs:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["favorite"], + "moji": "📑" + }, + "books": { + "unicode": "1F4DA", + "unicode_alternates": [], + "name": "books", + "shortname": ":books:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["library", "literature"], + "moji": "📚" + }, + "boom": { + "unicode": "1F4A5", + "unicode_alternates": [], + "name": "collision symbol", + "shortname": ":boom:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bomb", "explode", "explosion", "boom", "bang", "collision", "fire", "emphasis", "wow", "bam"], + "moji": "💥" + }, + "boot": { + "unicode": "1F462", + "unicode_alternates": [], + "name": "womans boots", + "shortname": ":boot:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "shoes"], + "moji": "👢" + }, + "bouquet": { + "unicode": "1F490", + "unicode_alternates": [], + "name": "bouquet", + "shortname": ":bouquet:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flowers", "nature"], + "moji": "ðŸ’" + }, + "bouquet2": { + "unicode": "1F395", + "unicode_alternates": [], + "name": "bouquet of flowers", + "shortname": ":bouquet2:", + "category": "celebration", + "aliases": [":bouquet_of_flowers:"], + "aliases_ascii": [], + "keywords": ["nature", "marriage", "wedding", "bride"] + }, + "bow": { + "unicode": "1F647", + "unicode_alternates": [], + "name": "person bowing deeply", + "shortname": ":bow:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["boy", "male", "man", "sorry", "bow", "respect", "curtsy", "bend"], + "moji": "🙇" + }, + "bowling": { + "unicode": "1F3B3", + "unicode_alternates": [], + "name": "bowling", + "shortname": ":bowling:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fun", "play", "sports", "bowl", "bowling", "ball", "pin", "strike", "spare", "game"], + "moji": "🎳" + }, + "boy": { + "unicode": "1F466", + "unicode_alternates": [], + "name": "boy", + "shortname": ":boy:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["guy", "male", "man"], + "moji": "👦" + }, + "boys_symbol": { + "unicode": "1F6C9", + "unicode_alternates": [], + "name": "boys symbol", + "shortname": ":boys_symbol:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["male", "child"] + }, + "bread": { + "unicode": "1F35E", + "unicode_alternates": [], + "name": "bread", + "shortname": ":bread:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["breakfast", "food", "toast", "wheat", "bread", "loaf", "yeast"], + "moji": "ðŸž" + }, + "bride_with_veil": { + "unicode": "1F470", + "unicode_alternates": [], + "name": "bride with veil", + "shortname": ":bride_with_veil:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["couple", "marriage", "wedding", "bride", "wedding", "planning", "veil", "gown", "dress", "engagement", "white"], + "moji": "👰" + }, + "bridge_at_night": { + "unicode": "1F309", + "unicode_alternates": [], + "name": "bridge at night", + "shortname": ":bridge_at_night:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "sanfrancisco", "bridge", "night", "water", "road", "evening", "suspension", "golden", "gate"], + "moji": "🌉" + }, + "briefcase": { + "unicode": "1F4BC", + "unicode_alternates": [], + "name": "briefcase", + "shortname": ":briefcase:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["business", "documents", "work"], + "moji": "💼" + }, + "broken_heart": { + "unicode": "1F494", + "unicode_alternates": [], + "name": "broken heart", + "shortname": ":broken_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["</3"], + "keywords": ["sad", "sorry"], + "moji": "💔" + }, + "bug": { + "unicode": "1F41B", + "unicode_alternates": [], + "name": "bug", + "shortname": ":bug:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["insect", "nature", "bug", "insect", "virus", "error"], + "moji": "ðŸ›" + }, + "bulb": { + "unicode": "1F4A1", + "unicode_alternates": [], + "name": "electric light bulb", + "shortname": ":bulb:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["electricity", "light", "idea", "bulb", "light"], + "moji": "💡" + }, + "bullettrain_front": { + "unicode": "1F685", + "unicode_alternates": [], + "name": "high-speed train with bullet nose", + "shortname": ":bullettrain_front:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "train", "bullet", "rail"], + "moji": "🚅" + }, + "bullettrain_side": { + "unicode": "1F684", + "unicode_alternates": [], + "name": "high-speed train", + "shortname": ":bullettrain_side:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "train", "bullet", "rail"], + "moji": "🚄" + }, + "bullhorn": { + "unicode": "1F56B", + "unicode_alternates": [], + "name": "bullhorn", + "shortname": ":bullhorn:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "noise", "announcement", "megaphone"] + }, + "bullhorn_waves": { + "unicode": "1F56C", + "unicode_alternates": [], + "name": "bullhorn with sound waves", + "shortname": ":bullhorn_waves:", + "category": "objects_symbols", + "aliases": [":bullhorn_with_sound_waves:"], + "aliases_ascii": [], + "keywords": ["sound", "noise", "announcement", "megaphone"] + }, + "bus": { + "unicode": "1F68C", + "unicode_alternates": [], + "name": "bus", + "shortname": ":bus:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["car", "transportation", "vehicle", "bus", "school", "city", "transportation", "public"], + "moji": "🚌" + }, + "busstop": { + "unicode": "1F68F", + "unicode_alternates": [], + "name": "bus stop", + "shortname": ":busstop:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "bus", "stop", "city", "transport", "transportation"], + "moji": "ðŸš" + }, + "bust_in_silhouette": { + "unicode": "1F464", + "unicode_alternates": [], + "name": "bust in silhouette", + "shortname": ":bust_in_silhouette:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["human", "man", "person", "user", "silhouette", "person", "user", "member", "account", "guest", "icon", "avatar", "profile", "me", "myself", "i"], + "moji": "👤" + }, + "busts_in_silhouette": { + "unicode": "1F465", + "unicode_alternates": [], + "name": "busts in silhouette", + "shortname": ":busts_in_silhouette:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["group", "human", "man", "person", "team", "user", "silhouette", "silhouettes", "people", "user", "members", "accounts", "relationship", "shadow"], + "moji": "👥" + }, + "cactus": { + "unicode": "1F335", + "unicode_alternates": [], + "name": "cactus", + "shortname": ":cactus:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "vegetable", "cactus", "desert", "drought", "spike", "poke"], + "moji": "🌵" + }, + "cake": { + "unicode": "1F370", + "unicode_alternates": [], + "name": "shortcake", + "shortname": ":cake:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "cake", "short", "dessert", "strawberry"], + "moji": "ðŸ°" + }, + "calculator": { + "unicode": "1F5A9", + "unicode_alternates": [], + "name": "pocket calculator", + "shortname": ":calculator:", + "category": "objects_symbols", + "aliases": [":pocket calculator:"], + "aliases_ascii": [], + "keywords": ["add", "subtract", "multiple", "divide", "scientific"] + }, + "calendar": { + "unicode": "1F4C6", + "unicode_alternates": [], + "name": "tear-off calendar", + "shortname": ":calendar:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["schedule"], + "moji": "📆" + }, + "calendar_spiral": { + "unicode": "1F5D3", + "unicode_alternates": [], + "name": "spiral calendar pad", + "shortname": ":calendar_spiral:", + "category": "objects_symbols", + "aliases": [":spiral_calendar_pad:"], + "aliases_ascii": [], + "keywords": ["schedule", "date", "day"] + }, + "calling": { + "unicode": "1F4F2", + "unicode_alternates": [], + "name": "mobile phone with rightwards arrow at left", + "shortname": ":calling:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["incoming", "iphone"], + "moji": "📲" + }, + "camel": { + "unicode": "1F42B", + "unicode_alternates": [], + "name": "bactrian camel", + "shortname": ":camel:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "hot", "nature", "bactrian", "camel", "hump", "desert", "central asia", "heat", "hot", "water", "hump day", "wednesday", "sex"], + "moji": "ðŸ«" + }, + "camera": { + "unicode": "1F4F7", + "unicode_alternates": [], + "name": "camera", + "shortname": ":camera:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gadgets", "photo"], + "moji": "📷" + }, + "camera_with_flash": { + "unicode": "1F4F8", + "unicode_alternates": [], + "name": "camera with flash", + "shortname": ":camera_with_flash:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "picture"] + }, + "camping": { + "unicode": "1F3D5", + "unicode_alternates": [], + "name": "camping", + "shortname": ":camping:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["outdoors", "nature", "wilderness", "roughing", "activity"] + }, + "cancellation_x": { + "unicode": "1F5D9", + "unicode_alternates": [], + "name": "cancellation x", + "shortname": ":cancellation_x:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cancel", "stop", "delete"] + }, + "cancer": { + "unicode": "264B", + "unicode_alternates": ["264B-FE0F"], + "name": "cancer", + "shortname": ":cancer:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cancer", "crab", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "moji": "♋" + }, + "candle": { + "unicode": "1F56F", + "unicode_alternates": [], + "name": "candle", + "shortname": ":candle:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["light", "wax"] + }, + "candy": { + "unicode": "1F36C", + "unicode_alternates": [], + "name": "candy", + "shortname": ":candy:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "snack", "candy", "sugar", "sweet", "hard"], + "moji": "ðŸ¬" + }, + "capital_abcd": { + "unicode": "1F520", + "unicode_alternates": [], + "name": "input symbol for latin capital letters", + "shortname": ":capital_abcd:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-square", "words"], + "moji": "🔠" + }, + "capricorn": { + "unicode": "2651", + "unicode_alternates": ["2651-FE0F"], + "name": "capricorn", + "shortname": ":capricorn:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["capricorn", "sea-goat", "goat-horned", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "moji": "♑" + }, + "card_box": { + "unicode": "1F5C3", + "unicode_alternates": [], + "name": "card file box", + "shortname": ":card_box:", + "category": "objects_symbols", + "aliases": [":card_file_box:"], + "aliases_ascii": [], + "keywords": ["index", "organization"] + }, + "card_index": { + "unicode": "1F4C7", + "unicode_alternates": [], + "name": "card index", + "shortname": ":card_index:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["business", "stationery"], + "moji": "📇" + }, + "carousel_horse": { + "unicode": "1F3A0", + "unicode_alternates": [], + "name": "carousel horse", + "shortname": ":carousel_horse:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["carnival", "horse", "photo", "carousel", "horse", "amusement", "park", "ride", "entertainment", "park", "fair"], + "moji": "🎠" + }, + "cartridge": { + "unicode": "1F5AD", + "unicode_alternates": [], + "name": "tape cartridge", + "shortname": ":cartridge:", + "category": "objects_symbols", + "aliases": [":tape_cartridge:"], + "aliases_ascii": [], + "keywords": ["oldschool", "save", "technology", "disk", "storage", "information", "computer", "drive", "megabyte"] + }, + "cat": { + "unicode": "1F431", + "unicode_alternates": [], + "name": "cat face", + "shortname": ":cat:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "meow"], + "moji": "ðŸ±" + }, + "cat2": { + "unicode": "1F408", + "unicode_alternates": [], + "name": "cat", + "shortname": ":cat2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "meow", "pet", "cat", "kitten", "meow"], + "moji": "ðŸˆ" + }, + "celtic_cross": { + "unicode": "1F548", + "unicode_alternates": [], + "name": "celtic cross", + "shortname": ":celtic_cross:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["religion", "symbol"] + }, + "chart": { + "unicode": "1F4B9", + "unicode_alternates": [], + "name": "chart with upwards trend and yen sign", + "shortname": ":chart:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph", "green-square"], + "moji": "💹" + }, + "chart_with_downwards_trend": { + "unicode": "1F4C9", + "unicode_alternates": [], + "name": "chart with downwards trend", + "shortname": ":chart_with_downwards_trend:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph"], + "moji": "📉" + }, + "chart_with_upwards_trend": { + "unicode": "1F4C8", + "unicode_alternates": [], + "name": "chart with upwards trend", + "shortname": ":chart_with_upwards_trend:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph"], + "moji": "📈" + }, + "checkered_flag": { + "unicode": "1F3C1", + "unicode_alternates": [], + "name": "chequered flag", + "shortname": ":checkered_flag:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["contest", "finishline", "gokart", "rase", "checkered", "chequred", "race", "flag", "finish", "complete", "end"], + "moji": "ðŸ" + }, + "cherries": { + "unicode": "1F352", + "unicode_alternates": [], + "name": "cherries", + "shortname": ":cherries:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "cherry", "cherries", "tree", "fruit", "pit"], + "moji": "ðŸ’" + }, + "cherry_blossom": { + "unicode": "1F338", + "unicode_alternates": [], + "name": "cherry blossom", + "shortname": ":cherry_blossom:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flower", "nature", "plant", "cherry", "blossom", "tree", "flower"], + "moji": "🌸" + }, + "chestnut": { + "unicode": "1F330", + "unicode_alternates": [], + "name": "chestnut", + "shortname": ":chestnut:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "squirrel", "chestnut", "roasted", "food", "tree"], + "moji": "🌰" + }, + "chicken": { + "unicode": "1F414", + "unicode_alternates": [], + "name": "chicken", + "shortname": ":chicken:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cluck", "chicken", "hen", "poultry", "livestock"], + "moji": "ðŸ”" + }, + "children_crossing": { + "unicode": "1F6B8", + "unicode_alternates": [], + "name": "children crossing", + "shortname": ":children_crossing:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["school", "children", "kids", "caution", "crossing", "street", "crosswalk", "slow"], + "moji": "🚸" + }, + "chipmunk": { + "unicode": "1F43F", + "unicode_alternates": [], + "name": "chipmunk", + "shortname": ":chipmunk:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"] + }, + "chocolate_bar": { + "unicode": "1F36B", + "unicode_alternates": [], + "name": "chocolate bar", + "shortname": ":chocolate_bar:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "snack", "chocolate", "bar", "candy", "coca", "hershey's"], + "moji": "ðŸ«" + }, + "christmas_tree": { + "unicode": "1F384", + "unicode_alternates": [], + "name": "christmas tree", + "shortname": ":christmas_tree:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["celebration", "december", "festival", "vacation", "xmas", "christmas", "xmas", "santa", "holiday", "winter", "december", "santa", "evergreen", "ornaments", "jesus", "gifts", "presents"], + "moji": "🎄" + }, + "church": { + "unicode": "26EA", + "unicode_alternates": ["26EA-FE0F"], + "name": "church", + "shortname": ":church:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "christ", "religion"], + "moji": "⛪" + }, + "cinema": { + "unicode": "1F3A6", + "unicode_alternates": [], + "name": "cinema", + "shortname": ":cinema:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "film", "movie", "record", "cinema", "movie", "theater", "motion", "picture"], + "moji": "🎦" + }, + "circus_tent": { + "unicode": "1F3AA", + "unicode_alternates": [], + "name": "circus tent", + "shortname": ":circus_tent:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["carnival", "festival", "party", "circus", "tent", "event", "carnival", "big", "top", "canvas"], + "moji": "🎪" + }, + "city_dusk": { + "unicode": "1F306", + "unicode_alternates": [], + "name": "cityscape at dusk", + "shortname": ":city_dusk:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "city", "scape", "sunset", "dusk", "lights", "evening", "metropolitan", "night", "dark"], + "moji": "🌆" + }, + "city_sunset": { + "unicode": "1F307", + "unicode_alternates": [], + "name": "sunset over buildings", + "shortname": ":city_sunset:", + "category": "places", + "aliases": [":city_sunrise:"], + "aliases_ascii": [], + "keywords": ["photo", "city", "scape", "sunrise", "dawn", "light", "morning", "metropolitan", "rise", "sun"], + "moji": "🌇" + }, + "cityscape": { + "unicode": "1F3D9", + "unicode_alternates": [], + "name": "cityscape", + "shortname": ":cityscape:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["skyscraper", "city", "view", "lights", "buiildings", "metropolis"] + }, + "clap": { + "unicode": "1F44F", + "unicode_alternates": [], + "name": "clapping hands sign", + "shortname": ":clap:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["applause", "congrats", "hands", "praise", "clapping", "appreciation", "approval", "sound", "encouragement", "enthusiasm"], + "moji": "ðŸ‘" + }, + "clapper": { + "unicode": "1F3AC", + "unicode_alternates": [], + "name": "clapper board", + "shortname": ":clapper:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["film", "movie", "record", "clapper", "board", "clapboard", "movie", "film", "take"], + "moji": "🎬" + }, + "classical_building": { + "unicode": "1F3DB", + "unicode_alternates": [], + "name": "classical building", + "shortname": ":classical_building:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["government", "architecture", "history", "iconic", "genre"] + }, + "clipboard": { + "unicode": "1F4CB", + "unicode_alternates": [], + "name": "clipboard", + "shortname": ":clipboard:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents", "stationery"], + "moji": "📋" + }, + "clock": { + "unicode": "1F570", + "unicode_alternates": [], + "name": "mantlepiece clock", + "shortname": ":clock:", + "category": "objects_symbols", + "aliases": [":mantlepiece_clock:"], + "aliases_ascii": [], + "keywords": ["time"] + }, + "clock1": { + "unicode": "1F550", + "unicode_alternates": [], + "name": "clock face one oclock", + "shortname": ":clock1:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "ðŸ•" + }, + "clock10": { + "unicode": "1F559", + "unicode_alternates": [], + "name": "clock face ten oclock", + "shortname": ":clock10:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕙" + }, + "clock1030": { + "unicode": "1F565", + "unicode_alternates": [], + "name": "clock face ten-thirty", + "shortname": ":clock1030:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕥" + }, + "clock11": { + "unicode": "1F55A", + "unicode_alternates": [], + "name": "clock face eleven oclock", + "shortname": ":clock11:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕚" + }, + "clock1130": { + "unicode": "1F566", + "unicode_alternates": [], + "name": "clock face eleven-thirty", + "shortname": ":clock1130:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕦" + }, + "clock12": { + "unicode": "1F55B", + "unicode_alternates": [], + "name": "clock face twelve oclock", + "shortname": ":clock12:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕛" + }, + "clock1230": { + "unicode": "1F567", + "unicode_alternates": [], + "name": "clock face twelve-thirty", + "shortname": ":clock1230:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"] + }, + "clock130": { + "unicode": "1F55C", + "unicode_alternates": [], + "name": "clock face one-thirty", + "shortname": ":clock130:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕜" + }, + "clock2": { + "unicode": "1F551", + "unicode_alternates": [], + "name": "clock face two oclock", + "shortname": ":clock2:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕑" + }, + "clock230": { + "unicode": "1F55D", + "unicode_alternates": [], + "name": "clock face two-thirty", + "shortname": ":clock230:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "ðŸ•" + }, + "clock3": { + "unicode": "1F552", + "unicode_alternates": [], + "name": "clock face three oclock", + "shortname": ":clock3:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕒" + }, + "clock330": { + "unicode": "1F55E", + "unicode_alternates": [], + "name": "clock face three-thirty", + "shortname": ":clock330:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕞" + }, + "clock4": { + "unicode": "1F553", + "unicode_alternates": [], + "name": "clock face four oclock", + "shortname": ":clock4:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕓" + }, + "clock430": { + "unicode": "1F55F", + "unicode_alternates": [], + "name": "clock face four-thirty", + "shortname": ":clock430:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕟" + }, + "clock5": { + "unicode": "1F554", + "unicode_alternates": [], + "name": "clock face five oclock", + "shortname": ":clock5:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕔" + }, + "clock530": { + "unicode": "1F560", + "unicode_alternates": [], + "name": "clock face five-thirty", + "shortname": ":clock530:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕠" + }, + "clock6": { + "unicode": "1F555", + "unicode_alternates": [], + "name": "clock face six oclock", + "shortname": ":clock6:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕕" + }, + "clock630": { + "unicode": "1F561", + "unicode_alternates": [], + "name": "clock face six-thirty", + "shortname": ":clock630:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕡" + }, + "clock7": { + "unicode": "1F556", + "unicode_alternates": [], + "name": "clock face seven oclock", + "shortname": ":clock7:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕖" + }, + "clock730": { + "unicode": "1F562", + "unicode_alternates": [], + "name": "clock face seven-thirty", + "shortname": ":clock730:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕢" + }, + "clock8": { + "unicode": "1F557", + "unicode_alternates": [], + "name": "clock face eight oclock", + "shortname": ":clock8:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕗" + }, + "clock830": { + "unicode": "1F563", + "unicode_alternates": [], + "name": "clock face eight-thirty", + "shortname": ":clock830:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕣" + }, + "clock9": { + "unicode": "1F558", + "unicode_alternates": [], + "name": "clock face nine oclock", + "shortname": ":clock9:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕘" + }, + "clock930": { + "unicode": "1F564", + "unicode_alternates": [], + "name": "clock face nine-thirty", + "shortname": ":clock930:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "time"], + "moji": "🕤" + }, + "clockwise_arrows": { + "unicode": "1F5D8", + "unicode_alternates": [], + "name": "clockwise right and left semicircle arrows", + "shortname": ":clockwise_arrows:", + "category": "objects_symbols", + "aliases": [":clockwise_right_and_left_semicircle_arrows:"], + "aliases_ascii": [], + "keywords": ["sync"] + }, + "closed_book": { + "unicode": "1F4D5", + "unicode_alternates": [], + "name": "closed book", + "shortname": ":closed_book:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["knowledge", "library", "read"], + "moji": "📕" + }, + "closed_lock_with_key": { + "unicode": "1F510", + "unicode_alternates": [], + "name": "closed lock with key", + "shortname": ":closed_lock_with_key:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["privacy", "security"], + "moji": "ðŸ”" + }, + "closed_umbrella": { + "unicode": "1F302", + "unicode_alternates": [], + "name": "closed umbrella", + "shortname": ":closed_umbrella:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["drizzle", "rain", "weather", "umbrella", "closed", "rain", "moisture", "protection", "sun", "ultraviolet", "uv"], + "moji": "🌂" + }, + "cloud": { + "unicode": "2601", + "unicode_alternates": ["2601-FE0F"], + "name": "cloud", + "shortname": ":cloud:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sky", "weather"], + "moji": "â˜" + }, + "cloud_lightning": { + "unicode": "1F329", + "unicode_alternates": [], + "name": "cloud with lightning", + "shortname": ":cloud_lightning:", + "category": "nature", + "aliases": [":cloud_with_lightning:"], + "aliases_ascii": [], + "keywords": ["weather", "thunder"] + }, + "cloud_rain": { + "unicode": "1F327", + "unicode_alternates": [], + "name": "cloud with rain", + "shortname": ":cloud_rain:", + "category": "nature", + "aliases": [":cloud_with_rain:"], + "aliases_ascii": [], + "keywords": ["weather", "wet"] + }, + "cloud_snow": { + "unicode": "1F328", + "unicode_alternates": [], + "name": "cloud with snow", + "shortname": ":cloud_snow:", + "category": "nature", + "aliases": [":cloud_with_snow:"], + "aliases_ascii": [], + "keywords": ["weather", "cold"] + }, + "cloud_tornado": { + "unicode": "1F32A", + "unicode_alternates": [], + "name": "cloud with tornado", + "shortname": ":cloud_tornado:", + "category": "nature", + "aliases": [":cloud_with_tornado:"], + "aliases_ascii": [], + "keywords": ["weather", "destruction", "funnel"] + }, + "clubs": { + "unicode": "2663", + "unicode_alternates": ["2663-FE0F"], + "name": "black club suit", + "shortname": ":clubs:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cards", "poker"], + "moji": "♣" + }, + "cocktail": { + "unicode": "1F378", + "unicode_alternates": [], + "name": "cocktail glass", + "shortname": ":cocktail:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alcohol", "beverage", "drink", "drunk", "cocktail", "mixed", "drink", "alcohol", "glass", "martini", "bar"], + "moji": "ðŸ¸" + }, + "coffee": { + "unicode": "2615", + "unicode_alternates": ["2615-FE0F"], + "name": "hot beverage", + "shortname": ":coffee:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beverage", "cafe", "drink", "espresso"], + "moji": "☕" + }, + "cold_sweat": { + "unicode": "1F630", + "unicode_alternates": [], + "name": "face with open mouth and cold sweat", + "shortname": ":cold_sweat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "nervous", "sweat", "exasperated", "frustrated"], + "moji": "😰" + }, + "compression": { + "unicode": "1F5DC", + "unicode_alternates": [], + "name": "compression", + "shortname": ":compression:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["reduce"] + }, + "computer": { + "unicode": "1F4BB", + "unicode_alternates": [], + "name": "personal computer", + "shortname": ":computer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["laptop", "tech"], + "moji": "💻" + }, + "computer_old": { + "unicode": "1F5B3", + "unicode_alternates": [], + "name": "old personal computer", + "shortname": ":computer_old:", + "category": "objects_symbols", + "aliases": [":old_personal_computer:"], + "aliases_ascii": [], + "keywords": ["cpu", "terminal"] + }, + "confetti_ball": { + "unicode": "1F38A", + "unicode_alternates": [], + "name": "confetti ball", + "shortname": ":confetti_ball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["festival", "party", "party", "congratulations", "confetti", "ball", "celebrate", "win", "birthday", "new years", "wedding"], + "moji": "🎊" + }, + "confounded": { + "unicode": "1F616", + "unicode_alternates": [], + "name": "confounded face", + "shortname": ":confounded:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["confused", "face", "sick", "unwell", "confound", "amaze", "perplex", "puzzle", "mystify"], + "moji": "😖" + }, + "confused": { + "unicode": "1F615", + "unicode_alternates": [], + "name": "confused face", + "shortname": ":confused:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [">:\\", ">:/", ":-/", ":-.", ":/", ":\\", "=/", "=\\", ":L", "=L"], + "keywords": ["confused", "confuse", "daze", "perplex", "puzzle", "indifference", "skeptical", "undecided", "uneasy", "hesitant"], + "moji": "😕" + }, + "congratulations": { + "unicode": "3297", + "unicode_alternates": ["3297-FE0F"], + "name": "circled ideograph congratulation", + "shortname": ":congratulations:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "japanese", "kanji"], + "moji": "㊗" + }, + "construction": { + "unicode": "1F6A7", + "unicode_alternates": [], + "name": "construction sign", + "shortname": ":construction:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["caution", "progress", "wip"], + "moji": "🚧" + }, + "construction_worker": { + "unicode": "1F477", + "unicode_alternates": [], + "name": "construction worker", + "shortname": ":construction_worker:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["human", "male", "man", "wip"], + "moji": "👷" + }, + "control_knobs": { + "unicode": "1F39B", + "unicode_alternates": [], + "name": "control knobs", + "shortname": ":control_knobs:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dial"] + }, + "contruction_site": { + "unicode": "1F3D7", + "unicode_alternates": [], + "name": "building construction", + "shortname": ":contruction_site:", + "category": "travel_places", + "aliases": [":building_construction:"], + "aliases_ascii": [], + "keywords": ["site", "work"] + }, + "convenience_store": { + "unicode": "1F3EA", + "unicode_alternates": [], + "name": "convenience store", + "shortname": ":convenience_store:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building"], + "moji": "ðŸª" + }, + "cookie": { + "unicode": "1F36A", + "unicode_alternates": [], + "name": "cookie", + "shortname": ":cookie:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chocolate", "food", "oreo", "snack", "cookie", "dessert", "biscuit", "sweet", "chocolate"], + "moji": "ðŸª" + }, + "cool": { + "unicode": "1F192", + "unicode_alternates": [], + "name": "squared cool", + "shortname": ":cool:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "words"], + "moji": "🆒" + }, + "cop": { + "unicode": "1F46E", + "unicode_alternates": [], + "name": "police officer", + "shortname": ":cop:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrest", "enforcement", "law", "man", "police"], + "moji": "👮" + }, + "copyright": { + "moji": "©", + "unicode": "00A9", + "unicode_alternates": [], + "name": "copyright sign", + "shortname": ":copyright:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ip", "license"] + }, + "corn": { + "unicode": "1F33D", + "unicode_alternates": [], + "name": "ear of maize", + "shortname": ":corn:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "plant", "vegetable", "corn", "maize", "food", "iowa", "kernel", "popcorn", "husk", "yellow", "stalk", "cob", "ear"], + "moji": "🌽" + }, + "couch": { + "unicode": "1F6CB", + "unicode_alternates": [], + "name": "couch and lamp", + "shortname": ":couch:", + "category": "travel_places", + "aliases": [":couch_and_lamp:"], + "aliases_ascii": [], + "keywords": ["lounge", "sectional", "sofa", "loveseat", "leather", "microfiber", "sit", "relax"] + }, + "couple": { + "unicode": "1F46B", + "unicode_alternates": [], + "name": "man and woman holding hands", + "shortname": ":couple:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "date", "dating", "human", "like", "love", "marriage", "people", "valentines"], + "moji": "👫" + }, + "couple_mm": { + "unicode": "1F468-2764-1F468", + "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F468"], + "name": "couple (man,man)", + "shortname": ":couple_mm:", + "category": "people", + "aliases": [":couple_with_heart_mm:"], + "aliases_ascii": [], + "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"] + }, + "couple_with_heart": { + "unicode": "1F491", + "unicode_alternates": [], + "name": "couple with heart", + "shortname": ":couple_with_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"], + "moji": "💑" + }, + "couple_ww": { + "unicode": "1F469-2764-1F469", + "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F469"], + "name": "couple (woman,woman)", + "shortname": ":couple_ww:", + "category": "people", + "aliases": [":couple_with_heart_ww:"], + "aliases_ascii": [], + "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"] + }, + "couplekiss": { + "unicode": "1F48F", + "unicode_alternates": [], + "name": "kiss", + "shortname": ":couplekiss:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dating", "like", "love", "marriage", "valentines"], + "moji": "ðŸ’" + }, + "cow": { + "unicode": "1F42E", + "unicode_alternates": [], + "name": "cow face", + "shortname": ":cow:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "beef", "ox"], + "moji": "ðŸ®" + }, + "cow2": { + "unicode": "1F404", + "unicode_alternates": [], + "name": "cow", + "shortname": ":cow2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "beef", "nature", "ox", "cow", "milk", "dairy", "beef", "bessie", "moo"], + "moji": "ðŸ„" + }, + "crayon": { + "unicode": "1F58D", + "unicode_alternates": [], + "name": "lower left crayon", + "shortname": ":crayon:", + "category": "objects_symbols", + "aliases": [":lower_left_crayon:"], + "aliases_ascii": [], + "keywords": ["write", "draw", "color", "wax"] + }, + "credit_card": { + "unicode": "1F4B3", + "unicode_alternates": [], + "name": "credit card", + "shortname": ":credit_card:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bill", "dollar", "money", "pay", "payment", "credit", "card", "loan", "purchase", "shopping", "mastercard", "visa", "american express", "wallet", "signature"], + "moji": "💳" + }, + "crescent_moon": { + "unicode": "1F319", + "unicode_alternates": [], + "name": "crescent moon", + "shortname": ":crescent_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "moon", "crescent", "waxing", "sky", "night", "cheese", "phase"], + "moji": "🌙" + }, + "crocodile": { + "unicode": "1F40A", + "unicode_alternates": [], + "name": "crocodile", + "shortname": ":crocodile:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "crocodile", "croc", "alligator", "gator", "cranky"], + "moji": "ðŸŠ" + }, + "cross_heavy": { + "unicode": "1F547", + "unicode_alternates": [], + "name": "heavy latin cross", + "shortname": ":cross_heavy:", + "category": "objects_symbols", + "aliases": [":heavy_latin_cross:"], + "aliases_ascii": [], + "keywords": ["religion", "symbol"] + }, + "cross_white": { + "unicode": "1F546", + "unicode_alternates": [], + "name": "white latin cross", + "shortname": ":cross_white:", + "category": "objects_symbols", + "aliases": [":white_latin_cross:"], + "aliases_ascii": [], + "keywords": ["religion", "symbol"] + }, + "crossbones": { + "unicode": "1F571", + "unicode_alternates": [], + "name": "black skull and crossbones", + "shortname": ":crossbones:", + "category": "objects_symbols", + "aliases": [":black_skull_and_crossbones:"], + "aliases_ascii": [], + "keywords": ["poison", "danger", "death"] + }, + "crossed_flags": { + "unicode": "1F38C", + "unicode_alternates": [], + "name": "crossed flags", + "shortname": ":crossed_flags:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japan"], + "moji": "🎌" + }, + "crown": { + "unicode": "1F451", + "unicode_alternates": [], + "name": "crown", + "shortname": ":crown:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["king", "kod", "leader", "royalty"], + "moji": "👑" + }, + "cruise_ship": { + "unicode": "1F6F3", + "unicode_alternates": [], + "name": "passenger ship", + "shortname": ":cruise_ship:", + "category": "travel_places", + "aliases": [":passenger_ship:"], + "aliases_ascii": [], + "keywords": ["titanic", "transportation", "boat"] + }, + "cry": { + "unicode": "1F622", + "unicode_alternates": [], + "name": "crying face", + "shortname": ":cry:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":'(", ":'-(", ";(", ";-("], + "keywords": ["face", "sad", "sad", "cry", "tear", "weep", "tears"], + "moji": "😢" + }, + "crying_cat_face": { + "unicode": "1F63F", + "unicode_alternates": [], + "name": "crying cat face", + "shortname": ":crying_cat_face:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "sad", "tears", "weep", "cry", "cat", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"], + "moji": "😿" + }, + "crystal_ball": { + "unicode": "1F52E", + "unicode_alternates": [], + "name": "crystal ball", + "shortname": ":crystal_ball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["disco", "party"], + "moji": "🔮" + }, + "cupid": { + "unicode": "1F498", + "unicode_alternates": [], + "name": "heart with arrow", + "shortname": ":cupid:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "heart", "like", "love", "valentines"], + "moji": "💘" + }, + "curly_loop": { + "unicode": "27B0", + "unicode_alternates": [], + "name": "curly loop", + "shortname": ":curly_loop:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["scribble"], + "moji": "âž°" + }, + "currency_exchange": { + "unicode": "1F4B1", + "unicode_alternates": [], + "name": "currency exchange", + "shortname": ":currency_exchange:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dollar", "money", "travel"], + "moji": "💱" + }, + "curry": { + "unicode": "1F35B", + "unicode_alternates": [], + "name": "curry and rice", + "shortname": ":curry:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "hot", "indian", "spicy", "curry", "spice", "flavor", "food", "meal"], + "moji": "ðŸ›" + }, + "custard": { + "unicode": "1F36E", + "unicode_alternates": [], + "name": "custard", + "shortname": ":custard:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "custard", "cream", "rich", "butter", "dessert", "crème", "brûlée", "french"], + "moji": "ðŸ®" + }, + "customs": { + "unicode": "1F6C3", + "unicode_alternates": [], + "name": "customs", + "shortname": ":customs:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["border", "passport", "customs", "travel", "foreign", "goods", "check", "authority", "government"], + "moji": "🛃" + }, + "cyclone": { + "moji": "🌀", + "unicode": "1F300", + "unicode_alternates": [], + "name": "cyclone", + "shortname": ":cyclone:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue", "cloud", "swirl", "weather", "cyclone", "hurricane", "typhoon", "storm", "ocean"] + }, + "dagger": { + "unicode": "1F5E1", + "unicode_alternates": [], + "name": "dagger knife", + "shortname": ":dagger:", + "category": "objects_symbols", + "aliases": [":dagger_knife:"], + "aliases_ascii": [], + "keywords": ["blade", "knife"] + }, + "dancer": { + "unicode": "1F483", + "unicode_alternates": [], + "name": "dancer", + "shortname": ":dancer:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "fun", "girl", "woman", "dance", "dancer", "dress", "fancy", "boogy", "party", "celebrate", "ballet", "tango", "cha cha", "music"], + "moji": "💃" + }, + "dancers": { + "unicode": "1F46F", + "unicode_alternates": [], + "name": "woman with bunny ears", + "shortname": ":dancers:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bunny", "female", "girls", "women", "dancing", "dancers", "showgirl", "playboy", "costume", "bunny", "cancan"], + "moji": "👯" + }, + "dango": { + "unicode": "1F361", + "unicode_alternates": [], + "name": "dango", + "shortname": ":dango:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "dango", "japanese", "dumpling", "mochi", "balls", "skewer"], + "moji": "ðŸ¡" + }, + "dark_sunglasses": { + "unicode": "1F576", + "unicode_alternates": [], + "name": "dark sunglasses", + "shortname": ":dark_sunglasses:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shades", "eyes"] + }, + "dart": { + "unicode": "1F3AF", + "unicode_alternates": [], + "name": "direct hit", + "shortname": ":dart:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bar", "game", "direct", "hit", "bullseye", "dart", "archery", "game", "fletching", "arrow", "sport"], + "moji": "🎯" + }, + "dash": { + "unicode": "1F4A8", + "unicode_alternates": [], + "name": "dash symbol", + "shortname": ":dash:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["air", "fast", "shoo", "wind"], + "moji": "💨" + }, + "date": { + "unicode": "1F4C5", + "unicode_alternates": [], + "name": "calendar", + "shortname": ":date:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["calendar", "schedule"], + "moji": "📅" + }, + "deciduous_tree": { + "unicode": "1F333", + "unicode_alternates": [], + "name": "deciduous tree", + "shortname": ":deciduous_tree:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "deciduous", "tree", "leaves", "fall", "color"], + "moji": "🌳" + }, + "department_store": { + "unicode": "1F3EC", + "unicode_alternates": [], + "name": "department store", + "shortname": ":department_store:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "mall", "shopping", "department", "store", "retail", "sale", "merchandise"], + "moji": "ðŸ¬" + }, + "descending_notes": { + "unicode": "1F39D", + "unicode_alternates": [], + "name": "beamed descending musical notes", + "shortname": ":descending_notes:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["score", "music", "sound", "tone"] + }, + "desert": { + "unicode": "1F3DC", + "unicode_alternates": [], + "name": "desert", + "shortname": ":desert:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hot", "dry", "sandy", "cactus", "sunny", "barren"] + }, + "desktop": { + "unicode": "1F5A5", + "unicode_alternates": [], + "name": "desktop computer", + "shortname": ":desktop:", + "category": "objects_symbols", + "aliases": [":desktop_computer:"], + "aliases_ascii": [], + "keywords": ["cpu"] + }, + "desktop_window": { + "unicode": "1F5D4", + "unicode_alternates": [], + "name": "desktop window", + "shortname": ":desktop_window:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["computer"] + }, + "diamond_shape_with_a_dot_inside": { + "unicode": "1F4A0", + "unicode_alternates": [], + "name": "diamond shape with a dot inside", + "shortname": ":diamond_shape_with_a_dot_inside:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["diamond", "cute", "cuteness", "kawaii", "japanese", "glyph", "adorable"], + "moji": "💠" + }, + "diamonds": { + "unicode": "2666", + "unicode_alternates": ["2666-FE0F"], + "name": "black diamond suit", + "shortname": ":diamonds:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cards", "poker"], + "moji": "♦" + }, + "disappointed": { + "unicode": "1F61E", + "unicode_alternates": [], + "name": "disappointed face", + "shortname": ":disappointed:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [">:[", ":-(", ":(", ":-[", ":[", "=("], + "keywords": ["disappointed", "disappoint", "frown", "depressed", "discouraged", "face", "sad", "upset"], + "moji": "😞" + }, + "disappointed_relieved": { + "unicode": "1F625", + "unicode_alternates": [], + "name": "disappointed but relieved face", + "shortname": ":disappointed_relieved:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "nervous", "phew", "sweat", "disappoint", "relief"], + "moji": "😥" + }, + "dividers": { + "unicode": "1F5C2", + "unicode_alternates": [], + "name": "card index dividers", + "shortname": ":dividers:", + "category": "objects_symbols", + "aliases": [":card_index_dividers:"], + "aliases_ascii": [], + "keywords": ["stationery", "rolodex"] + }, + "dizzy": { + "unicode": "1F4AB", + "unicode_alternates": [], + "name": "dizzy symbol", + "shortname": ":dizzy:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shoot", "sparkle", "star", "dizzy", "drunk", "sick", "intoxicated", "squeans", "starburst", "star"], + "moji": "💫" + }, + "dizzy_face": { + "unicode": "1F635", + "unicode_alternates": [], + "name": "dizzy face", + "shortname": ":dizzy_face:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["#-)", "#)", "%-)", "%)", "X)", "X-)"], + "keywords": ["dizzy", "drunk", "inebriated", "face", "spent", "unconscious", "xox"], + "moji": "😵" + }, + "do_not_litter": { + "unicode": "1F6AF", + "unicode_alternates": [], + "name": "do not litter symbol", + "shortname": ":do_not_litter:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bin", "garbage", "trash", "litter", "garbage", "waste", "no", "can", "trash"], + "moji": "🚯" + }, + "document": { + "unicode": "1F5CE", + "unicode_alternates": [], + "name": "document", + "shortname": ":document:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["page"] + }, + "document_text": { + "unicode": "1F5B9", + "unicode_alternates": [], + "name": "document with text", + "shortname": ":document_text:", + "category": "objects_symbols", + "aliases": [":document_with_text:"], + "aliases_ascii": [], + "keywords": ["page"] + }, + "dog": { + "unicode": "1F436", + "unicode_alternates": [], + "name": "dog face", + "shortname": ":dog:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "friend", "nature", "woof"], + "moji": "ðŸ¶" + }, + "dog2": { + "unicode": "1F415", + "unicode_alternates": [], + "name": "dog", + "shortname": ":dog2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "doge", "friend", "nature", "pet", "dog", "puppy", "pet", "friend", "woof", "bark", "fido"], + "moji": "ðŸ•" + }, + "dollar": { + "unicode": "1F4B5", + "unicode_alternates": [], + "name": "banknote with dollar sign", + "shortname": ":dollar:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bill", "currency", "money", "dollar", "united states", "canada", "australia", "banknote", "money", "currency", "paper", "cash", "bills"], + "moji": "💵" + }, + "dolls": { + "unicode": "1F38E", + "unicode_alternates": [], + "name": "japanese dolls", + "shortname": ":dolls:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japanese", "kimono", "toy", "dolls", "japan", "japanese", "day", "girls", "emperor", "empress", "pray", "blessing", "imperial", "family", "royal"], + "moji": "🎎" + }, + "dolphin": { + "unicode": "1F42C", + "unicode_alternates": [], + "name": "dolphin", + "shortname": ":dolphin:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "fins", "fish", "flipper", "nature", "ocean", "sea"], + "moji": "ðŸ¬" + }, + "door": { + "unicode": "1F6AA", + "unicode_alternates": [], + "name": "door", + "shortname": ":door:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["entry", "exit", "house", "door", "doorway", "entrance", "enter", "exit", "entry"], + "moji": "🚪" + }, + "doughnut": { + "unicode": "1F369", + "unicode_alternates": [], + "name": "doughnut", + "shortname": ":doughnut:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "snack", "sweet", "doughnut", "donut", "pastry", "fried", "dessert", "breakfast", "police", "homer", "sweet"], + "moji": "ðŸ©" + }, + "dove": { + "unicode": "1F54A", + "unicode_alternates": [], + "name": "dove of peace", + "shortname": ":dove:", + "category": "objects_symbols", + "aliases": [":dove_of_peace:"], + "aliases_ascii": [], + "keywords": ["symbol", "bird"] + }, + "dragon": { + "unicode": "1F409", + "unicode_alternates": [], + "name": "dragon", + "shortname": ":dragon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "fire", "legendary", "myth"], + "moji": "ðŸ‰" + }, + "dragon_face": { + "unicode": "1F432", + "unicode_alternates": [], + "name": "dragon face", + "shortname": ":dragon_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "head", "fire", "legendary", "myth"], + "moji": "ðŸ²" + }, + "dress": { + "unicode": "1F457", + "unicode_alternates": [], + "name": "dress", + "shortname": ":dress:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clothes", "fashion"], + "moji": "👗" + }, + "dromedary_camel": { + "unicode": "1F42A", + "unicode_alternates": [], + "name": "dromedary camel", + "shortname": ":dromedary_camel:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "desert", "hot", "dromedary", "camel", "hump", "desert", "middle east", "heat", "hot", "water", "hump day", "wednesday", "sex"], + "moji": "ðŸª" + }, + "droplet": { + "unicode": "1F4A7", + "unicode_alternates": [], + "name": "droplet", + "shortname": ":droplet:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["drip", "faucet", "water", "drop", "droplet", "h20", "water", "aqua", "tear", "sweat", "rain", "moisture", "wet", "moist", "spit"], + "moji": "💧" + }, + "dvd": { + "unicode": "1F4C0", + "unicode_alternates": [], + "name": "dvd", + "shortname": ":dvd:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cd", "disc", "disk"], + "moji": "📀" + }, + "e-mail": { + "unicode": "1F4E7", + "unicode_alternates": [], + "name": "e-mail symbol", + "shortname": ":e-mail:", + "category": "objects", + "aliases": [":email:"], + "aliases_ascii": [], + "keywords": ["communication", "inbox"], + "moji": "📧" + }, + "ear": { + "unicode": "1F442", + "unicode_alternates": [], + "name": "ear", + "shortname": ":ear:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "hear", "listen", "sound"], + "moji": "👂" + }, + "ear_of_rice": { + "unicode": "1F33E", + "unicode_alternates": [], + "name": "ear of rice", + "shortname": ":ear_of_rice:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "ear", "rice", "food", "plant", "seed"], + "moji": "🌾" + }, + "earth_africa": { + "unicode": "1F30D", + "unicode_alternates": [], + "name": "earth globe europe-africa", + "shortname": ":earth_africa:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["globe", "international", "world", "earth", "globe", "space", "planet", "africa", "europe", "home"], + "moji": "ðŸŒ" + }, + "earth_americas": { + "unicode": "1F30E", + "unicode_alternates": [], + "name": "earth globe americas", + "shortname": ":earth_americas:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["USA", "globe", "international", "world", "earth", "globe", "space", "planet", "north", "south", "america", "americas", "home"], + "moji": "🌎" + }, + "earth_asia": { + "unicode": "1F30F", + "unicode_alternates": [], + "name": "earth globe asia-australia", + "shortname": ":earth_asia:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["east", "globe", "international", "world", "earth", "globe", "space", "planet", "asia", "australia", "home"], + "moji": "ðŸŒ" + }, + "egg": { + "unicode": "1F373", + "unicode_alternates": [], + "name": "cooking", + "shortname": ":egg:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["breakfast", "food", "egg", "fry", "pan", "flat", "cook", "frying", "cooking", "utensil"], + "moji": "ðŸ³" + }, + "eggplant": { + "unicode": "1F346", + "unicode_alternates": [], + "name": "aubergine", + "shortname": ":eggplant:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["aubergine", "food", "nature", "vegetable", "eggplant", "aubergine", "fruit", "purple", "penis"], + "moji": "ðŸ†" + }, + "eight": { + "moji": "8ï¸âƒ£", + "unicode": "0038-20E3", + "unicode_alternates": ["0038-FE0F-20E3"], + "name": "digit eight", + "shortname": ":eight:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["8", "blue-square", "numbers"] + }, + "eight_pointed_black_star": { + "unicode": "2734", + "unicode_alternates": ["2734-FE0F"], + "name": "eight pointed black star", + "shortname": ":eight_pointed_black_star:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "✴" + }, + "eight_spoked_asterisk": { + "unicode": "2733", + "unicode_alternates": ["2733-FE0F"], + "name": "eight spoked asterisk", + "shortname": ":eight_spoked_asterisk:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["green-square", "sparkle", "star"], + "moji": "✳" + }, + "electric_plug": { + "unicode": "1F50C", + "unicode_alternates": [], + "name": "electric plug", + "shortname": ":electric_plug:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["charger", "power"], + "moji": "🔌" + }, + "elephant": { + "unicode": "1F418", + "unicode_alternates": [], + "name": "elephant", + "shortname": ":elephant:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "nose", "thailand"], + "moji": "ðŸ˜" + }, + "end": { + "unicode": "1F51A", + "unicode_alternates": [], + "name": "end with leftwards arrow above", + "shortname": ":end:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "words"], + "moji": "🔚" + }, + "envelope": { + "unicode": "2709", + "unicode_alternates": ["2709-FE0F"], + "name": "envelope", + "shortname": ":envelope:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "letter", "mail", "postal"], + "moji": "✉" + }, + "envelope_back": { + "unicode": "1F582", + "unicode_alternates": [], + "name": "back of envelope", + "shortname": ":envelope_back:", + "category": "objects_symbols", + "aliases": [":back_of_envelope:"], + "aliases_ascii": [], + "keywords": ["communication", "letter", "mail", "postal"] + }, + "envelope_flying": { + "unicode": "1F585", + "unicode_alternates": [], + "name": "flying envelope", + "shortname": ":envelope_flying:", + "category": "objects_symbols", + "aliases": [":flying_envelope:"], + "aliases_ascii": [], + "keywords": ["communication", "letter", "mail", "postal"] + }, + "envelope_stamped": { + "unicode": "1F583", + "unicode_alternates": [], + "name": "stamped envelope", + "shortname": ":envelope_stamped:", + "category": "objects_symbols", + "aliases": [":stamped_envelope:"], + "aliases_ascii": [], + "keywords": ["communication", "letter", "mail", "postal"] + }, + "envelope_stamped_pen": { + "unicode": "1F586", + "unicode_alternates": [], + "name": "pen over stamped envelope", + "shortname": ":envelope_stamped_pen:", + "category": "objects_symbols", + "aliases": [":pen_over_stamped_envelope:"], + "aliases_ascii": [], + "keywords": ["communication", "letter", "mail", "postal"] + }, + "envelope_with_arrow": { + "unicode": "1F4E9", + "unicode_alternates": [], + "name": "envelope with downwards arrow above", + "shortname": ":envelope_with_arrow:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["email"], + "moji": "📩" + }, + "euro": { + "unicode": "1F4B6", + "unicode_alternates": [], + "name": "banknote with euro sign", + "shortname": ":euro:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["currency", "dollar", "money", "euro", "europe", "banknote", "money", "currency", "paper", "cash", "bills"], + "moji": "💶" + }, + "european_castle": { + "unicode": "1F3F0", + "unicode_alternates": [], + "name": "european castle", + "shortname": ":european_castle:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "history", "royalty", "castle", "european", "residence", "royalty", "disneyland", "disney", "fort", "fortified", "moat", "tower", "princess", "prince", "lord", "king", "queen", "fortress", "nobel", "stronghold"], + "moji": "ðŸ°" + }, + "european_post_office": { + "unicode": "1F3E4", + "unicode_alternates": [], + "name": "european post office", + "shortname": ":european_post_office:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building"], + "moji": "ðŸ¤" + }, + "evergreen_tree": { + "unicode": "1F332", + "unicode_alternates": [], + "name": "evergreen tree", + "shortname": ":evergreen_tree:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "evergreen", "tree", "needles", "christmas"], + "moji": "🌲" + }, + "exclamation": { + "unicode": "2757", + "unicode_alternates": ["2757-FE0F"], + "name": "heavy exclamation mark symbol", + "shortname": ":exclamation:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["surprise"], + "moji": "â—" + }, + "expressionless": { + "unicode": "1F611", + "unicode_alternates": [], + "name": "expressionless face", + "shortname": ":expressionless:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["-_-", "-__-", "-___-"], + "keywords": ["expressionless", "blank", "void", "vapid", "without expression", "face", "indifferent"], + "moji": "😑" + }, + "eye": { + "unicode": "1F441", + "unicode_alternates": [], + "name": "eye", + "shortname": ":eye:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["look", "peek", "watch"] + }, + "eyeglasses": { + "unicode": "1F453", + "unicode_alternates": [], + "name": "eyeglasses", + "shortname": ":eyeglasses:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "eyesight", "fashion", "eyeglasses", "spectacles", "eye", "sight", "nearsightedness", "myopia", "farsightedness", "hyperopia", "frames", "vision", "see", "blurry", "contacts"], + "moji": "👓" + }, + "eyes": { + "unicode": "1F440", + "unicode_alternates": [], + "name": "eyes", + "shortname": ":eyes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["look", "peek", "stalk", "watch"], + "moji": "👀" + }, + "factory": { + "unicode": "1F3ED", + "unicode_alternates": [], + "name": "factory", + "shortname": ":factory:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building"], + "moji": "ðŸ" + }, + "fallen_leaf": { + "unicode": "1F342", + "unicode_alternates": [], + "name": "fallen leaf", + "shortname": ":fallen_leaf:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["leaves", "nature", "plant", "vegetable", "leaf", "fall", "color", "deciduous", "autumn"], + "moji": "ðŸ‚" + }, + "family": { + "unicode": "1F46A", + "unicode_alternates": [], + "name": "family", + "shortname": ":family:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["child", "dad", "father", "home", "mom", "mother", "parents", "family", "mother", "father", "child", "girl", "boy", "group", "unit"], + "moji": "👪" + }, + "family_mmb": { + "unicode": "1F468-1F468-1F466", + "unicode_alternates": ["1F468-200D-1F468-200D-1F466"], + "name": "family (man,man,boy)", + "shortname": ":family_mmb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"] + }, + "family_mmbb": { + "unicode": "1F468-1F468-1F466-1F466", + "unicode_alternates": ["1F468-200D-1F468-200D-1F466-200D-1F466"], + "name": "family (man,man,boy,boy)", + "shortname": ":family_mmbb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"] + }, + "family_mmg": { + "unicode": "1F468-1F468-1F467", + "unicode_alternates": ["1F468-200D-1F468-200D-1F467"], + "name": "family (man,man,girl)", + "shortname": ":family_mmg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"] + }, + "family_mmgb": { + "unicode": "1F468-1F468-1F467-1F466", + "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F466"], + "name": "family (man,man,girl,boy)", + "shortname": ":family_mmgb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl", "boy"] + }, + "family_mmgg": { + "unicode": "1F468-1F468-1F467-1F467", + "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F467"], + "name": "family (man,man,girl,girl)", + "shortname": ":family_mmgg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"] + }, + "family_mwbb": { + "unicode": "1F468-1F469-1F466-1F466", + "unicode_alternates": ["1F468-200D-1F469-200D-1F466-200D-1F466"], + "name": "family (man,woman,boy,boy)", + "shortname": ":family_mwbb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dad", "father", "mom", "mother", "parents", "children", "boy", "group", "unit", "man", "woman"] + }, + "family_mwg": { + "unicode": "1F468-1F469-1F467", + "unicode_alternates": ["1F468-200D-1F469-200D-1F467"], + "name": "family (man,woman,girl)", + "shortname": ":family_mwg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["child", "dad", "father", "mom", "mother", "parents", "girl", "boy", "group", "unit", "man", "woman"] + }, + "family_mwgb": { + "unicode": "1F468-1F469-1F467-1F466", + "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F466"], + "name": "family (man,woman,girl,boy)", + "shortname": ":family_mwgb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "boy", "group", "unit", "man", "woman"] + }, + "family_mwgg": { + "unicode": "1F468-1F469-1F467-1F467", + "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F467"], + "name": "family (man,woman,girl,girl)", + "shortname": ":family_mwgg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "group", "unit", "man", "woman"] + }, + "family_wwb": { + "unicode": "1F469-1F469-1F466", + "unicode_alternates": ["1F469-200D-1F469-200D-1F466"], + "name": "family (woman,woman,boy)", + "shortname": ":family_wwb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mom", "mother", "parents", "child", "boy", "group", "unit", "gay", "lesbian", "homosexual", "woman"] + }, + "family_wwbb": { + "unicode": "1F469-1F469-1F466-1F466", + "unicode_alternates": ["1F469-200D-1F469-200D-1F466-200D-1F466"], + "name": "family (woman,woman,boy,boy)", + "shortname": ":family_wwbb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "boy"] + }, + "family_wwg": { + "unicode": "1F469-1F469-1F467", + "unicode_alternates": ["1F469-200D-1F469-200D-1F467"], + "name": "family (woman,woman,girl)", + "shortname": ":family_wwg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mom", "mother", "parents", "child", "woman", "girl", "group", "unit", "gay", "lesbian", "homosexual"] + }, + "family_wwgb": { + "unicode": "1F469-1F469-1F467-1F466", + "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F466"], + "name": "family (woman,woman,girl,boy)", + "shortname": ":family_wwgb:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl", "boy"] + }, + "family_wwgg": { + "unicode": "1F469-1F469-1F467-1F467", + "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F467"], + "name": "family (woman,woman,girl,girl)", + "shortname": ":family_wwgg:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl"] + }, + "fast_forward": { + "unicode": "23E9", + "unicode_alternates": [], + "name": "black right-pointing double triangle", + "shortname": ":fast_forward:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "â©" + }, + "fax": { + "unicode": "1F4E0", + "unicode_alternates": [], + "name": "fax machine", + "shortname": ":fax:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "technology"], + "moji": "📠" + }, + "fearful": { + "unicode": "1F628", + "unicode_alternates": [], + "name": "fearful face", + "shortname": ":fearful:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "nervous", "oops", "scared", "terrified", "fear", "fearful", "scared", "frightened"], + "moji": "😨" + }, + "feet": { + "unicode": "1F43E", + "unicode_alternates": [], + "name": "paw prints", + "shortname": ":feet:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cat", "dog", "footprints", "paw", "pet", "tracking", "paw", "prints", "mark", "imprints", "footsteps", "animal", "lion", "bear", "dog", "cat", "raccoon", "critter", "feet", "pawsteps"], + "moji": "ðŸ¾" + }, + "ferris_wheel": { + "unicode": "1F3A1", + "unicode_alternates": [], + "name": "ferris wheel", + "shortname": ":ferris_wheel:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["carnival", "londoneye", "photo", "farris", "wheel", "amusement", "park", "fair", "ride", "entertainment"], + "moji": "🎡" + }, + "file_cabinet": { + "unicode": "1F5C4", + "unicode_alternates": [], + "name": "file cabinet", + "shortname": ":file_cabinet:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["folders", "office", "documents", "storage"] + }, + "file_folder": { + "unicode": "1F4C1", + "unicode_alternates": [], + "name": "file folder", + "shortname": ":file_folder:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"], + "moji": "ðŸ“" + }, + "film_frames": { + "unicode": "1F39E", + "unicode_alternates": [], + "name": "film frames", + "shortname": ":film_frames:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": ["movie", "record", "8mm", "16mm", "reel", "celluloid"] + }, + "finger_pointing_down": { + "unicode": "1F597", + "unicode_alternates": [], + "name": "white down pointing left hand index", + "shortname": ":finger_pointing_down:", + "category": "people", + "aliases": [":white_down_pointing_left_hand_index:"], + "aliases_ascii": [], + "keywords": ["direction", "finger", "hand"] + }, + "finger_pointing_down2": { + "unicode": "1F59F", + "unicode_alternates": [], + "name": "sideways white down pointing index", + "shortname": ":finger_pointing_down2:", + "category": "people", + "aliases": [":sideways_white_down_pointing_index:"], + "aliases_ascii": [], + "keywords": ["direction", "finger", "hand"] + }, + "finger_pointing_left": { + "unicode": "1F598", + "unicode_alternates": [], + "name": "sideways white left pointing index", + "shortname": ":finger_pointing_left:", + "category": "people", + "aliases": [":sideways_white_left_pointing_index:"], + "aliases_ascii": [], + "keywords": ["direction", "finger", "hand"] + }, + "finger_pointing_right": { + "unicode": "1F599", + "unicode_alternates": [], + "name": "sideways white right pointing index", + "shortname": ":finger_pointing_right:", + "category": "people", + "aliases": [":sideways_white_right_pointing_index:"], + "aliases_ascii": [], + "keywords": ["direction", "finger", "hand"] + }, + "finger_pointing_up": { + "unicode": "1F59E", + "unicode_alternates": [], + "name": "sideways white up pointing index", + "shortname": ":finger_pointing_up:", + "category": "people", + "aliases": [":sideways_white_up_pointing_index:"], + "aliases_ascii": [], + "keywords": ["direction", "finger", "hand"] + }, + "fire": { + "unicode": "1F525", + "unicode_alternates": [], + "name": "fire", + "shortname": ":fire:", + "category": "emoticons", + "aliases": [":flame:"], + "aliases_ascii": [], + "keywords": ["cook", "hot", "flame"], + "moji": "🔥" + }, + "fire_engine": { + "unicode": "1F692", + "unicode_alternates": [], + "name": "fire engine", + "shortname": ":fire_engine:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "transportation", "vehicle", "fire", "fighter", "engine", "truck", "emergency", "medical"], + "moji": "🚒" + }, + "fire_engine_oncoming": { + "unicode": "1F6F1", + "unicode_alternates": [], + "name": "oncoming fire engine", + "shortname": ":fire_engine_oncoming:", + "category": "travel_places", + "aliases": [":oncoming_fire_engine:"], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "fighter", "truck", "emergency"] + }, + "fireworks": { + "unicode": "1F386", + "unicode_alternates": [], + "name": "fireworks", + "shortname": ":fireworks:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["carnival", "congratulations", "festival", "photo", "fireworks", "independence", "celebration", "explosion", "july", "4th", "rocket", "sky", "idea", "excitement"], + "moji": "🎆" + }, + "first_quarter_moon": { + "unicode": "1F313", + "unicode_alternates": [], + "name": "first quarter moon symbol", + "shortname": ":first_quarter_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "quarter", "first", "sky", "night", "cheese", "phase"], + "moji": "🌓" + }, + "first_quarter_moon_with_face": { + "unicode": "1F31B", + "unicode_alternates": [], + "name": "first quarter moon with face", + "shortname": ":first_quarter_moon_with_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "first", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "moji": "🌛" + }, + "fish": { + "unicode": "1F41F", + "unicode_alternates": [], + "name": "fish", + "shortname": ":fish:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "food", "nature"], + "moji": "ðŸŸ" + }, + "fish_cake": { + "unicode": "1F365", + "unicode_alternates": [], + "name": "fish cake with swirl design", + "shortname": ":fish_cake:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fish", "cake", "kamboko", "swirl", "ramen", "noodles", "naruto"], + "moji": "ðŸ¥" + }, + "fishing_pole_and_fish": { + "unicode": "1F3A3", + "unicode_alternates": [], + "name": "fishing pole and fish", + "shortname": ":fishing_pole_and_fish:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "hobby", "fish", "fishing", "pole"], + "moji": "🎣" + }, + "fist": { + "unicode": "270A", + "unicode_alternates": [], + "name": "raised fist", + "shortname": ":fist:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fingers", "grasp", "hand"], + "moji": "✊" + }, + "five": { + "moji": "5ï¸âƒ£", + "unicode": "0035-20E3", + "unicode_alternates": ["0035-FE0F-20E3"], + "name": "digit five", + "shortname": ":five:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "numbers", "prime"] + }, + "flag_ac": { + "unicode": "1F1E6-1F1E8", + "unicode_alternates": [], + "name": "ascension", + "shortname": ":flag_ac:", + "category": "flags", + "aliases": [":ac:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ac"] + }, + "flag_ad": { + "unicode": "1F1E6-1F1E9", + "unicode_alternates": [], + "name": "andorra", + "shortname": ":flag_ad:", + "category": "flags", + "aliases": [":ad:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ad"] + }, + "flag_ae": { + "unicode": "1F1E6-1F1EA", + "unicode_alternates": [], + "name": "the united arab emirates", + "shortname": ":flag_ae:", + "category": "flags", + "aliases": [":ae:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ae"] + }, + "flag_af": { + "unicode": "1F1E6-1F1EB", + "unicode_alternates": [], + "name": "afghanistan", + "shortname": ":flag_af:", + "category": "flags", + "aliases": [":af:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "afghanestan", "af"] + }, + "flag_ag": { + "unicode": "1F1E6-1F1EC", + "unicode_alternates": [], + "name": "antigua and barbuda", + "shortname": ":flag_ag:", + "category": "flags", + "aliases": [":ag:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ag"] + }, + "flag_ai": { + "unicode": "1F1E6-1F1EE", + "unicode_alternates": [], + "name": "anguilla", + "shortname": ":flag_ai:", + "category": "flags", + "aliases": [":ai:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ai"] + }, + "flag_al": { + "unicode": "1F1E6-1F1F1", + "unicode_alternates": [], + "name": "albania", + "shortname": ":flag_al:", + "category": "flags", + "aliases": [":al:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "shqiperia", "al"] + }, + "flag_am": { + "unicode": "1F1E6-1F1F2", + "unicode_alternates": [], + "name": "armenia", + "shortname": ":flag_am:", + "category": "flags", + "aliases": [":am:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "hayastan", "am"] + }, + "flag_ao": { + "unicode": "1F1E6-1F1F4", + "unicode_alternates": [], + "name": "angola", + "shortname": ":flag_ao:", + "category": "flags", + "aliases": [":ao:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ao"] + }, + "flag_ar": { + "unicode": "1F1E6-1F1F7", + "unicode_alternates": [], + "name": "argentina", + "shortname": ":flag_ar:", + "category": "flags", + "aliases": [":ar:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ar"] + }, + "flag_at": { + "unicode": "1F1E6-1F1F9", + "unicode_alternates": [], + "name": "austria", + "shortname": ":flag_at:", + "category": "flags", + "aliases": [":at:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "österreich", "osterreich", "at"] + }, + "flag_au": { + "unicode": "1F1E6-1F1FA", + "unicode_alternates": [], + "name": "australia", + "shortname": ":flag_au:", + "category": "flags", + "aliases": [":au:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "au"] + }, + "flag_aw": { + "unicode": "1F1E6-1F1FC", + "unicode_alternates": [], + "name": "aruba", + "shortname": ":flag_aw:", + "category": "flags", + "aliases": [":aw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "aw"] + }, + "flag_az": { + "unicode": "1F1E6-1F1FF", + "unicode_alternates": [], + "name": "azerbaijan", + "shortname": ":flag_az:", + "category": "flags", + "aliases": [":az:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "azarbaycan", "az"] + }, + "flag_ba": { + "unicode": "1F1E7-1F1E6", + "unicode_alternates": [], + "name": "bosnia and herzegovina", + "shortname": ":flag_ba:", + "category": "flags", + "aliases": [":ba:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bosna i hercegovina", "ba"] + }, + "flag_bb": { + "unicode": "1F1E7-1F1E7", + "unicode_alternates": [], + "name": "barbados", + "shortname": ":flag_bb:", + "category": "flags", + "aliases": [":bb:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bb"] + }, + "flag_bd": { + "unicode": "1F1E7-1F1E9", + "unicode_alternates": [], + "name": "bangladesh", + "shortname": ":flag_bd:", + "category": "flags", + "aliases": [":bd:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bd"] + }, + "flag_be": { + "unicode": "1F1E7-1F1EA", + "unicode_alternates": [], + "name": "belgium", + "shortname": ":flag_be:", + "category": "flags", + "aliases": [":be:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "belgique", "belgie", "be"] + }, + "flag_bf": { + "unicode": "1F1E7-1F1EB", + "unicode_alternates": [], + "name": "burkina faso", + "shortname": ":flag_bf:", + "category": "flags", + "aliases": [":bf:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bf"] + }, + "flag_bg": { + "unicode": "1F1E7-1F1EC", + "unicode_alternates": [], + "name": "bulgaria", + "shortname": ":flag_bg:", + "category": "flags", + "aliases": [":bg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bg"] + }, + "flag_bh": { + "unicode": "1F1E7-1F1ED", + "unicode_alternates": [], + "name": "bahrain", + "shortname": ":flag_bh:", + "category": "flags", + "aliases": [":bh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al bahrayn", "bh"] + }, + "flag_bi": { + "unicode": "1F1E7-1F1EE", + "unicode_alternates": [], + "name": "burundi", + "shortname": ":flag_bi:", + "category": "flags", + "aliases": [":bi:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bi"] + }, + "flag_bj": { + "unicode": "1F1E7-1F1EF", + "unicode_alternates": [], + "name": "benin", + "shortname": ":flag_bj:", + "category": "flags", + "aliases": [":bj:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bj"] + }, + "flag_black": { + "unicode": "1F3F4", + "unicode_alternates": [], + "name": "waving black flag", + "shortname": ":flag_black:", + "category": "objects_symbols", + "aliases": [":waving_black_flag:"], + "aliases_ascii": [], + "keywords": ["symbol", "signal"] + }, + "flag_bm": { + "unicode": "1F1E7-1F1F2", + "unicode_alternates": [], + "name": "bermuda", + "shortname": ":flag_bm:", + "category": "flags", + "aliases": [":bm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bm"] + }, + "flag_bn": { + "unicode": "1F1E7-1F1F3", + "unicode_alternates": [], + "name": "brunei", + "shortname": ":flag_bn:", + "category": "flags", + "aliases": [":bn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bn"] + }, + "flag_bo": { + "unicode": "1F1E7-1F1F4", + "unicode_alternates": [], + "name": "bolivia", + "shortname": ":flag_bo:", + "category": "flags", + "aliases": [":bo:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bo"] + }, + "flag_br": { + "unicode": "1F1E7-1F1F7", + "unicode_alternates": [], + "name": "brazil", + "shortname": ":flag_br:", + "category": "flags", + "aliases": [":br:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "brasil", "br"] + }, + "flag_bs": { + "unicode": "1F1E7-1F1F8", + "unicode_alternates": [], + "name": "the bahamas", + "shortname": ":flag_bs:", + "category": "flags", + "aliases": [":bs:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bs"] + }, + "flag_bt": { + "unicode": "1F1E7-1F1F9", + "unicode_alternates": [], + "name": "bhutan", + "shortname": ":flag_bt:", + "category": "flags", + "aliases": [":bt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bt"] + }, + "flag_bw": { + "unicode": "1F1E7-1F1FC", + "unicode_alternates": [], + "name": "botswana", + "shortname": ":flag_bw:", + "category": "flags", + "aliases": [":bw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bw"] + }, + "flag_by": { + "unicode": "1F1E7-1F1FE", + "unicode_alternates": [], + "name": "belarus", + "shortname": ":flag_by:", + "category": "flags", + "aliases": [":by:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "byelarus", "by"] + }, + "flag_bz": { + "unicode": "1F1E7-1F1FF", + "unicode_alternates": [], + "name": "belize", + "shortname": ":flag_bz:", + "category": "flags", + "aliases": [":bz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bz"] + }, + "flag_ca": { + "unicode": "1F1E8-1F1E6", + "unicode_alternates": [], + "name": "canada", + "shortname": ":flag_ca:", + "category": "flags", + "aliases": [":ca:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ca"] + }, + "flag_cd": { + "unicode": "1F1E8-1F1E9", + "unicode_alternates": [], + "name": "the democratic republic of the congo", + "shortname": ":flag_cd:", + "category": "flags", + "aliases": [":congo:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "république démocratique du congo", "republique democratique du congo", "cd"] + }, + "flag_cf": { + "unicode": "1F1E8-1F1EB", + "unicode_alternates": [], + "name": "central african republic", + "shortname": ":flag_cf:", + "category": "flags", + "aliases": [":cf:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cf"] + }, + "flag_cg": { + "unicode": "1F1E8-1F1EC", + "unicode_alternates": [], + "name": "the republic of the congo", + "shortname": ":flag_cg:", + "category": "flags", + "aliases": [":cg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cg"] + }, + "flag_ch": { + "unicode": "1F1E8-1F1ED", + "unicode_alternates": [], + "name": "switzerland", + "shortname": ":flag_ch:", + "category": "flags", + "aliases": [":ch:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "swiss"] + }, + "flag_ci": { + "unicode": "1F1E8-1F1EE", + "unicode_alternates": [], + "name": "cote d'ivoire", + "shortname": ":flag_ci:", + "category": "flags", + "aliases": [":ci:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ci"] + }, + "flag_cl": { + "unicode": "1F1E8-1F1F1", + "unicode_alternates": [], + "name": "chile", + "shortname": ":flag_cl:", + "category": "flags", + "aliases": [":chile:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cl"] + }, + "flag_cm": { + "unicode": "1F1E8-1F1F2", + "unicode_alternates": [], + "name": "cameroon", + "shortname": ":flag_cm:", + "category": "flags", + "aliases": [":cm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cm"] + }, + "flag_cn": { + "unicode": "1F1E8-1F1F3", + "unicode_alternates": [], + "name": "china", + "shortname": ":flag_cn:", + "category": "flags", + "aliases": [":cn:"], + "aliases_ascii": [], + "keywords": ["chinese", "prc", "zhong guo", "country", "nation", "cn"] + }, + "flag_co": { + "unicode": "1F1E8-1F1F4", + "unicode_alternates": [], + "name": "colombia", + "shortname": ":flag_co:", + "category": "flags", + "aliases": [":co:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "co"] + }, + "flag_cr": { + "unicode": "1F1E8-1F1F7", + "unicode_alternates": [], + "name": "costa rica", + "shortname": ":flag_cr:", + "category": "flags", + "aliases": [":cr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cr"] + }, + "flag_cu": { + "unicode": "1F1E8-1F1FA", + "unicode_alternates": [], + "name": "cuba", + "shortname": ":flag_cu:", + "category": "flags", + "aliases": [":cu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cu"] + }, + "flag_cv": { + "unicode": "1F1E8-1F1FB", + "unicode_alternates": [], + "name": "cape verde", + "shortname": ":flag_cv:", + "category": "flags", + "aliases": [":cv:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "cabo verde", "cv"] + }, + "flag_cy": { + "unicode": "1F1E8-1F1FE", + "unicode_alternates": [], + "name": "cyprus", + "shortname": ":flag_cy:", + "category": "flags", + "aliases": [":cy:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kibris", "kypros", "cy"] + }, + "flag_cz": { + "unicode": "1F1E8-1F1FF", + "unicode_alternates": [], + "name": "the czech republic", + "shortname": ":flag_cz:", + "category": "flags", + "aliases": [":cz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ceska republika", "cz"] + }, + "flag_de": { + "unicode": "1F1E9-1F1EA", + "unicode_alternates": [], + "name": "germany", + "shortname": ":flag_de:", + "category": "flags", + "aliases": [":de:"], + "aliases_ascii": [], + "keywords": ["german", "nation", "deutschland", "country", "de"] + }, + "flag_dj": { + "unicode": "1F1E9-1F1EF", + "unicode_alternates": [], + "name": "djibouti", + "shortname": ":flag_dj:", + "category": "flags", + "aliases": [":dj:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "dj"] + }, + "flag_dk": { + "unicode": "1F1E9-1F1F0", + "unicode_alternates": [], + "name": "denmark", + "shortname": ":flag_dk:", + "category": "flags", + "aliases": [":dk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "danmark", "dk"] + }, + "flag_dm": { + "unicode": "1F1E9-1F1F2", + "unicode_alternates": [], + "name": "dominica", + "shortname": ":flag_dm:", + "category": "flags", + "aliases": [":dm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "dm"] + }, + "flag_do": { + "unicode": "1F1E9-1F1F4", + "unicode_alternates": [], + "name": "the dominican republic", + "shortname": ":flag_do:", + "category": "flags", + "aliases": [":do:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "do"] + }, + "flag_dz": { + "unicode": "1F1E9-1F1FF", + "unicode_alternates": [], + "name": "algeria", + "shortname": ":flag_dz:", + "category": "flags", + "aliases": [":dz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al jaza'ir", "al jazair", "dz"] + }, + "flag_ec": { + "unicode": "1F1EA-1F1E8", + "unicode_alternates": [], + "name": "ecuador", + "shortname": ":flag_ec:", + "category": "flags", + "aliases": [":ec:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ec"] + }, + "flag_ee": { + "unicode": "1F1EA-1F1EA", + "unicode_alternates": [], + "name": "estonia", + "shortname": ":flag_ee:", + "category": "flags", + "aliases": [":ee:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "eesti vabariik", "ee"] + }, + "flag_eg": { + "unicode": "1F1EA-1F1EC", + "unicode_alternates": [], + "name": "egypt", + "shortname": ":flag_eg:", + "category": "flags", + "aliases": [":eg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "misr", "eg"] + }, + "flag_eh": { + "unicode": "1F1EA-1F1ED", + "unicode_alternates": [], + "name": "western sahara", + "shortname": ":flag_eh:", + "category": "flags", + "aliases": [":eh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "aá¹£-á¹¢aḥrÄ’ al-gharbÄ«yah", "sahra", "gharbiyah", "eh"] + }, + "flag_er": { + "unicode": "1F1EA-1F1F7", + "unicode_alternates": [], + "name": "eritrea", + "shortname": ":flag_er:", + "category": "flags", + "aliases": [":er:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "hagere ertra", "er"] + }, + "flag_es": { + "unicode": "1F1EA-1F1F8", + "unicode_alternates": [], + "name": "spain", + "shortname": ":flag_es:", + "category": "flags", + "aliases": [":es:"], + "aliases_ascii": [], + "keywords": ["nation", "españa", "country", "espana", "es"] + }, + "flag_et": { + "unicode": "1F1EA-1F1F9", + "unicode_alternates": [], + "name": "ethiopia", + "shortname": ":flag_et:", + "category": "flags", + "aliases": [":et:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ityop'iya", "ityopiya", "et"] + }, + "flag_fi": { + "unicode": "1F1EB-1F1EE", + "unicode_alternates": [], + "name": "finland", + "shortname": ":flag_fi:", + "category": "flags", + "aliases": [":fi:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "suomen tasavalta", "fi"] + }, + "flag_fj": { + "unicode": "1F1EB-1F1EF", + "unicode_alternates": [], + "name": "fiji", + "shortname": ":flag_fj:", + "category": "flags", + "aliases": [":fj:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "fj"] + }, + "flag_fk": { + "unicode": "1F1EB-1F1F0", + "unicode_alternates": [], + "name": "falkland islands", + "shortname": ":flag_fk:", + "category": "flags", + "aliases": [":fk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "islas malvinas", "fk"] + }, + "flag_fm": { + "unicode": "1F1EB-1F1F2", + "unicode_alternates": [], + "name": "micronesia", + "shortname": ":flag_fm:", + "category": "flags", + "aliases": [":fm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "fm"] + }, + "flag_fo": { + "unicode": "1F1EB-1F1F4", + "unicode_alternates": [], + "name": "faroe islands", + "shortname": ":flag_fo:", + "category": "flags", + "aliases": [":fo:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "foroyar", "fo"] + }, + "flag_fr": { + "unicode": "1F1EB-1F1F7", + "unicode_alternates": [], + "name": "france", + "shortname": ":flag_fr:", + "category": "flags", + "aliases": [":fr:"], + "aliases_ascii": [], + "keywords": ["french", "nation", "country", "fr"] + }, + "flag_ga": { + "unicode": "1F1EC-1F1E6", + "unicode_alternates": [], + "name": "gabon", + "shortname": ":flag_ga:", + "category": "flags", + "aliases": [":ga:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ga"] + }, + "flag_gb": { + "unicode": "1F1EC-1F1E7", + "unicode_alternates": [], + "name": "great britain", + "shortname": ":flag_gb:", + "category": "flags", + "aliases": [":gb:"], + "aliases_ascii": [], + "keywords": ["UK", "gb", "britsh", "nation", "united kingdom", "england", "country"] + }, + "flag_gd": { + "unicode": "1F1EC-1F1E9", + "unicode_alternates": [], + "name": "grenada", + "shortname": ":flag_gd:", + "category": "flags", + "aliases": [":gd:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gd"] + }, + "flag_ge": { + "unicode": "1F1EC-1F1EA", + "unicode_alternates": [], + "name": "georgia", + "shortname": ":flag_ge:", + "category": "flags", + "aliases": [":ge:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sak'art'velo", "sakartvelo", "ge"] + }, + "flag_gh": { + "unicode": "1F1EC-1F1ED", + "unicode_alternates": [], + "name": "ghana", + "shortname": ":flag_gh:", + "category": "flags", + "aliases": [":gh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gh"] + }, + "flag_gi": { + "unicode": "1F1EC-1F1EE", + "unicode_alternates": [], + "name": "gibraltar", + "shortname": ":flag_gi:", + "category": "flags", + "aliases": [":gi:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gi"] + }, + "flag_gl": { + "unicode": "1F1EC-1F1F1", + "unicode_alternates": [], + "name": "greenland", + "shortname": ":flag_gl:", + "category": "flags", + "aliases": [":gl:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kalaallit nunaat", "gl"] + }, + "flag_gm": { + "unicode": "1F1EC-1F1F2", + "unicode_alternates": [], + "name": "the gambia", + "shortname": ":flag_gm:", + "category": "flags", + "aliases": [":gm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gm"] + }, + "flag_gn": { + "unicode": "1F1EC-1F1F3", + "unicode_alternates": [], + "name": "guinea", + "shortname": ":flag_gn:", + "category": "flags", + "aliases": [":gn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "guinee", "gn"] + }, + "flag_gq": { + "unicode": "1F1EC-1F1F6", + "unicode_alternates": [], + "name": "equatorial guinea", + "shortname": ":flag_gq:", + "category": "flags", + "aliases": [":gq:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "guinea ecuatorial", "gq"] + }, + "flag_gr": { + "unicode": "1F1EC-1F1F7", + "unicode_alternates": [], + "name": "greece", + "shortname": ":flag_gr:", + "category": "flags", + "aliases": [":gr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ellas", "ellada", "gr"] + }, + "flag_gt": { + "unicode": "1F1EC-1F1F9", + "unicode_alternates": [], + "name": "guatemala", + "shortname": ":flag_gt:", + "category": "flags", + "aliases": [":gt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gt"] + }, + "flag_gu": { + "unicode": "1F1EC-1F1FA", + "unicode_alternates": [], + "name": "guam", + "shortname": ":flag_gu:", + "category": "flags", + "aliases": [":gu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gu"] + }, + "flag_gw": { + "unicode": "1F1EC-1F1FC", + "unicode_alternates": [], + "name": "guinea-bissau", + "shortname": ":flag_gw:", + "category": "flags", + "aliases": [":gw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "guine-bissau", "guine bissau", "gw"] + }, + "flag_gy": { + "unicode": "1F1EC-1F1FE", + "unicode_alternates": [], + "name": "guyana", + "shortname": ":flag_gy:", + "category": "flags", + "aliases": [":gy:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "gy"] + }, + "flag_hk": { + "unicode": "1F1ED-1F1F0", + "unicode_alternates": [], + "name": "hong kong", + "shortname": ":flag_hk:", + "category": "flags", + "aliases": [":hk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "xianggang", "hk"] + }, + "flag_hn": { + "unicode": "1F1ED-1F1F3", + "unicode_alternates": [], + "name": "honduras", + "shortname": ":flag_hn:", + "category": "flags", + "aliases": [":hn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "hn"] + }, + "flag_hr": { + "unicode": "1F1ED-1F1F7", + "unicode_alternates": [], + "name": "croatia", + "shortname": ":flag_hr:", + "category": "flags", + "aliases": [":hr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "hrvatska", "hr"] + }, + "flag_ht": { + "unicode": "1F1ED-1F1F9", + "unicode_alternates": [], + "name": "haiti", + "shortname": ":flag_ht:", + "category": "flags", + "aliases": [":ht:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ht"] + }, + "flag_hu": { + "unicode": "1F1ED-1F1FA", + "unicode_alternates": [], + "name": "hungary", + "shortname": ":flag_hu:", + "category": "flags", + "aliases": [":hu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "magyarorszag", "hu"] + }, + "flag_id": { + "unicode": "1F1EE-1F1E9", + "unicode_alternates": [], + "name": "indonesia", + "shortname": ":flag_id:", + "category": "flags", + "aliases": [":indonesia:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "id"] + }, + "flag_ie": { + "unicode": "1F1EE-1F1EA", + "unicode_alternates": [], + "name": "ireland", + "shortname": ":flag_ie:", + "category": "flags", + "aliases": [":ie:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "éire", "eire", "ie"] + }, + "flag_il": { + "unicode": "1F1EE-1F1F1", + "unicode_alternates": [], + "name": "israel", + "shortname": ":flag_il:", + "category": "flags", + "aliases": [":il:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "yisra'el", "yisrael", "il"] + }, + "flag_in": { + "unicode": "1F1EE-1F1F3", + "unicode_alternates": [], + "name": "india", + "shortname": ":flag_in:", + "category": "flags", + "aliases": [":in:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "bharat", "in"] + }, + "flag_iq": { + "unicode": "1F1EE-1F1F6", + "unicode_alternates": [], + "name": "iraq", + "shortname": ":flag_iq:", + "category": "flags", + "aliases": [":iq:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "iq"] + }, + "flag_ir": { + "unicode": "1F1EE-1F1F7", + "unicode_alternates": [], + "name": "iran", + "shortname": ":flag_ir:", + "category": "flags", + "aliases": [":ir:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ir"] + }, + "flag_is": { + "unicode": "1F1EE-1F1F8", + "unicode_alternates": [], + "name": "iceland", + "shortname": ":flag_is:", + "category": "flags", + "aliases": [":is:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lyoveldio island", "is"] + }, + "flag_it": { + "unicode": "1F1EE-1F1F9", + "unicode_alternates": [], + "name": "italy", + "shortname": ":flag_it:", + "category": "flags", + "aliases": [":it:"], + "aliases_ascii": [], + "keywords": ["italia", "country", "nation", "it"] + }, + "flag_je": { + "unicode": "1F1EF-1F1EA", + "unicode_alternates": [], + "name": "jersey", + "shortname": ":flag_je:", + "category": "flags", + "aliases": [":je:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "je"] + }, + "flag_jm": { + "unicode": "1F1EF-1F1F2", + "unicode_alternates": [], + "name": "jamaica", + "shortname": ":flag_jm:", + "category": "flags", + "aliases": [":jm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "jm"] + }, + "flag_jo": { + "unicode": "1F1EF-1F1F4", + "unicode_alternates": [], + "name": "jordan", + "shortname": ":flag_jo:", + "category": "flags", + "aliases": [":jo:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al urdun", "jo"] + }, + "flag_jp": { + "unicode": "1F1EF-1F1F5", + "unicode_alternates": [], + "name": "japan", + "shortname": ":flag_jp:", + "category": "flags", + "aliases": [":jp:"], + "aliases_ascii": [], + "keywords": ["nation", "nippon", "country", "jp"] + }, + "flag_ke": { + "unicode": "1F1F0-1F1EA", + "unicode_alternates": [], + "name": "kenya", + "shortname": ":flag_ke:", + "category": "flags", + "aliases": [":ke:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ke"] + }, + "flag_kg": { + "unicode": "1F1F0-1F1EC", + "unicode_alternates": [], + "name": "kyrgyzstan", + "shortname": ":flag_kg:", + "category": "flags", + "aliases": [":kg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kyrgyz respublikasy", "kg"] + }, + "flag_kh": { + "unicode": "1F1F0-1F1ED", + "unicode_alternates": [], + "name": "cambodia", + "shortname": ":flag_kh:", + "category": "flags", + "aliases": [":kh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kampuchea", "kh"] + }, + "flag_ki": { + "unicode": "1F1F0-1F1EE", + "unicode_alternates": [], + "name": "kiribati", + "shortname": ":flag_ki:", + "category": "flags", + "aliases": [":ki:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kiribati", "kiribas", "ki"] + }, + "flag_km": { + "unicode": "1F1F0-1F1F2", + "unicode_alternates": [], + "name": "the comoros", + "shortname": ":flag_km:", + "category": "flags", + "aliases": [":km:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "km"] + }, + "flag_kn": { + "unicode": "1F1F0-1F1F3", + "unicode_alternates": [], + "name": "saint kitts and nevis", + "shortname": ":flag_kn:", + "category": "flags", + "aliases": [":kn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kn"] + }, + "flag_kp": { + "unicode": "1F1F0-1F1F5", + "unicode_alternates": [], + "name": "north korea", + "shortname": ":flag_kp:", + "category": "flags", + "aliases": [":kp:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "kp"] + }, + "flag_kr": { + "unicode": "1F1F0-1F1F7", + "unicode_alternates": [], + "name": "korea", + "shortname": ":flag_kr:", + "category": "flags", + "aliases": [":kr:"], + "aliases_ascii": [], + "keywords": ["nation", "country", "south korea", "kr"] + }, + "flag_kw": { + "unicode": "1F1F0-1F1FC", + "unicode_alternates": [], + "name": "kuwait", + "shortname": ":flag_kw:", + "category": "flags", + "aliases": [":kw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al kuwayt", "kw"] + }, + "flag_ky": { + "unicode": "1F1F0-1F1FE", + "unicode_alternates": [], + "name": "cayman islands", + "shortname": ":flag_ky:", + "category": "flags", + "aliases": [":ky:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ky"] + }, + "flag_kz": { + "unicode": "1F1F0-1F1FF", + "unicode_alternates": [], + "name": "kazakhstan", + "shortname": ":flag_kz:", + "category": "flags", + "aliases": [":kz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "qazaqstan", "kz"] + }, + "flag_la": { + "unicode": "1F1F1-1F1E6", + "unicode_alternates": [], + "name": "laos", + "shortname": ":flag_la:", + "category": "flags", + "aliases": [":la:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "la"] + }, + "flag_lb": { + "unicode": "1F1F1-1F1E7", + "unicode_alternates": [], + "name": "lebanon", + "shortname": ":flag_lb:", + "category": "flags", + "aliases": [":lb:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lubnan", "lb"] + }, + "flag_lc": { + "unicode": "1F1F1-1F1E8", + "unicode_alternates": [], + "name": "saint lucia", + "shortname": ":flag_lc:", + "category": "flags", + "aliases": [":lc:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lc"] + }, + "flag_li": { + "unicode": "1F1F1-1F1EE", + "unicode_alternates": [], + "name": "liechtenstein", + "shortname": ":flag_li:", + "category": "flags", + "aliases": [":li:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "li"] + }, + "flag_lk": { + "unicode": "1F1F1-1F1F0", + "unicode_alternates": [], + "name": "sri lanka", + "shortname": ":flag_lk:", + "category": "flags", + "aliases": [":lk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lk"] + }, + "flag_lr": { + "unicode": "1F1F1-1F1F7", + "unicode_alternates": [], + "name": "liberia", + "shortname": ":flag_lr:", + "category": "flags", + "aliases": [":lr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lr"] + }, + "flag_ls": { + "unicode": "1F1F1-1F1F8", + "unicode_alternates": [], + "name": "lesotho", + "shortname": ":flag_ls:", + "category": "flags", + "aliases": [":ls:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ls"] + }, + "flag_lt": { + "unicode": "1F1F1-1F1F9", + "unicode_alternates": [], + "name": "lithuania", + "shortname": ":flag_lt:", + "category": "flags", + "aliases": [":lt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "lietuva", "lt"] + }, + "flag_lu": { + "unicode": "1F1F1-1F1FA", + "unicode_alternates": [], + "name": "luxembourg", + "shortname": ":flag_lu:", + "category": "flags", + "aliases": [":lu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "luxembourg", "letzebuerg", "lu"] + }, + "flag_lv": { + "unicode": "1F1F1-1F1FB", + "unicode_alternates": [], + "name": "latvia", + "shortname": ":flag_lv:", + "category": "flags", + "aliases": [":lv:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "latvija", "lv"] + }, + "flag_ly": { + "unicode": "1F1F1-1F1FE", + "unicode_alternates": [], + "name": "libya", + "shortname": ":flag_ly:", + "category": "flags", + "aliases": [":ly:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "libiyah", "ly"] + }, + "flag_ma": { + "unicode": "1F1F2-1F1E6", + "unicode_alternates": [], + "name": "morocco", + "shortname": ":flag_ma:", + "category": "flags", + "aliases": [":ma:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al maghrib", "ma"] + }, + "flag_mc": { + "unicode": "1F1F2-1F1E8", + "unicode_alternates": [], + "name": "monaco", + "shortname": ":flag_mc:", + "category": "flags", + "aliases": [":mc:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mc"] + }, + "flag_md": { + "unicode": "1F1F2-1F1E9", + "unicode_alternates": [], + "name": "moldova", + "shortname": ":flag_md:", + "category": "flags", + "aliases": [":md:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "md"] + }, + "flag_me": { + "unicode": "1F1F2-1F1EA", + "unicode_alternates": [], + "name": "montenegro", + "shortname": ":flag_me:", + "category": "flags", + "aliases": [":me:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "crna gora", "me"] + }, + "flag_mg": { + "unicode": "1F1F2-1F1EC", + "unicode_alternates": [], + "name": "madagascar", + "shortname": ":flag_mg:", + "category": "flags", + "aliases": [":mg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mg"] + }, + "flag_mh": { + "unicode": "1F1F2-1F1ED", + "unicode_alternates": [], + "name": "the marshall islands", + "shortname": ":flag_mh:", + "category": "flags", + "aliases": [":mh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mh"] + }, + "flag_mk": { + "unicode": "1F1F2-1F1F0", + "unicode_alternates": [], + "name": "macedonia", + "shortname": ":flag_mk:", + "category": "flags", + "aliases": [":mk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mk"] + }, + "flag_ml": { + "unicode": "1F1F2-1F1F1", + "unicode_alternates": [], + "name": "mali", + "shortname": ":flag_ml:", + "category": "flags", + "aliases": [":ml:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ml"] + }, + "flag_mm": { + "unicode": "1F1F2-1F1F2", + "unicode_alternates": [], + "name": "myanmar", + "shortname": ":flag_mm:", + "category": "flags", + "aliases": [":mm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "myanma naingngandaw", "mm"] + }, + "flag_mn": { + "unicode": "1F1F2-1F1F3", + "unicode_alternates": [], + "name": "mongolia", + "shortname": ":flag_mn:", + "category": "flags", + "aliases": [":mn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mongol uls", "mn"] + }, + "flag_mo": { + "unicode": "1F1F2-1F1F4", + "unicode_alternates": [], + "name": "macau", + "shortname": ":flag_mo:", + "category": "flags", + "aliases": [":mo:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "aomen", "mo"] + }, + "flag_mr": { + "unicode": "1F1F2-1F1F7", + "unicode_alternates": [], + "name": "mauritania", + "shortname": ":flag_mr:", + "category": "flags", + "aliases": [":mr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "muritaniyah", "mr"] + }, + "flag_ms": { + "unicode": "1F1F2-1F1F8", + "unicode_alternates": [], + "name": "montserrat", + "shortname": ":flag_ms:", + "category": "flags", + "aliases": [":ms:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ms"] + }, + "flag_mt": { + "unicode": "1F1F2-1F1F9", + "unicode_alternates": [], + "name": "malta", + "shortname": ":flag_mt:", + "category": "flags", + "aliases": [":mt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mt"] + }, + "flag_mu": { + "unicode": "1F1F2-1F1FA", + "unicode_alternates": [], + "name": "mauritius", + "shortname": ":flag_mu:", + "category": "flags", + "aliases": [":mu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mu"] + }, + "flag_mv": { + "unicode": "1F1F2-1F1FB", + "unicode_alternates": [], + "name": "maldives", + "shortname": ":flag_mv:", + "category": "flags", + "aliases": [":mv:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "dhivehi raajje", "mv"] + }, + "flag_mw": { + "unicode": "1F1F2-1F1FC", + "unicode_alternates": [], + "name": "malawi", + "shortname": ":flag_mw:", + "category": "flags", + "aliases": [":mw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mw"] + }, + "flag_mx": { + "unicode": "1F1F2-1F1FD", + "unicode_alternates": [], + "name": "mexico", + "shortname": ":flag_mx:", + "category": "flags", + "aliases": [":mx:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mx"] + }, + "flag_my": { + "unicode": "1F1F2-1F1FE", + "unicode_alternates": [], + "name": "malaysia", + "shortname": ":flag_my:", + "category": "flags", + "aliases": [":my:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "my"] + }, + "flag_mz": { + "unicode": "1F1F2-1F1FF", + "unicode_alternates": [], + "name": "mozambique", + "shortname": ":flag_mz:", + "category": "flags", + "aliases": [":mz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "mocambique", "mz"] + }, + "flag_na": { + "unicode": "1F1F3-1F1E6", + "unicode_alternates": [], + "name": "namibia", + "shortname": ":flag_na:", + "category": "flags", + "aliases": [":na:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "na"] + }, + "flag_nc": { + "unicode": "1F1F3-1F1E8", + "unicode_alternates": [], + "name": "new caledonia", + "shortname": ":flag_nc:", + "category": "flags", + "aliases": [":nc:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "nouvelle", "calédonie", "caledonie", "nc"] + }, + "flag_ne": { + "unicode": "1F1F3-1F1EA", + "unicode_alternates": [], + "name": "niger", + "shortname": ":flag_ne:", + "category": "flags", + "aliases": [":ne:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ne"] + }, + "flag_ng": { + "unicode": "1F1F3-1F1EC", + "unicode_alternates": [], + "name": "nigeria", + "shortname": ":flag_ng:", + "category": "flags", + "aliases": [":nigeria:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ng"] + }, + "flag_ni": { + "unicode": "1F1F3-1F1EE", + "unicode_alternates": [], + "name": "nicaragua", + "shortname": ":flag_ni:", + "category": "flags", + "aliases": [":ni:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ni"] + }, + "flag_nl": { + "unicode": "1F1F3-1F1F1", + "unicode_alternates": [], + "name": "the netherlands", + "shortname": ":flag_nl:", + "category": "flags", + "aliases": [":nl:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "nederland", "holland", "nl"] + }, + "flag_no": { + "unicode": "1F1F3-1F1F4", + "unicode_alternates": [], + "name": "norway", + "shortname": ":flag_no:", + "category": "flags", + "aliases": [":no:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "norge", "no"] + }, + "flag_np": { + "unicode": "1F1F3-1F1F5", + "unicode_alternates": [], + "name": "nepal", + "shortname": ":flag_np:", + "category": "flags", + "aliases": [":np:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "np"] + }, + "flag_nr": { + "unicode": "1F1F3-1F1F7", + "unicode_alternates": [], + "name": "nauru", + "shortname": ":flag_nr:", + "category": "flags", + "aliases": [":nr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "nr"] + }, + "flag_nu": { + "unicode": "1F1F3-1F1FA", + "unicode_alternates": [], + "name": "niue", + "shortname": ":flag_nu:", + "category": "flags", + "aliases": [":nu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "nu"] + }, + "flag_nz": { + "unicode": "1F1F3-1F1FF", + "unicode_alternates": [], + "name": "new zealand", + "shortname": ":flag_nz:", + "category": "flags", + "aliases": [":nz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "aotearoa", "nz"] + }, + "flag_om": { + "unicode": "1F1F4-1F1F2", + "unicode_alternates": [], + "name": "oman", + "shortname": ":flag_om:", + "category": "flags", + "aliases": [":om:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "saltanat uman", "om"] + }, + "flag_pa": { + "unicode": "1F1F5-1F1E6", + "unicode_alternates": [], + "name": "panama", + "shortname": ":flag_pa:", + "category": "flags", + "aliases": [":pa:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pa"] + }, + "flag_pe": { + "unicode": "1F1F5-1F1EA", + "unicode_alternates": [], + "name": "peru", + "shortname": ":flag_pe:", + "category": "flags", + "aliases": [":pe:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pe"] + }, + "flag_pf": { + "unicode": "1F1F5-1F1EB", + "unicode_alternates": [], + "name": "french polynesia", + "shortname": ":flag_pf:", + "category": "flags", + "aliases": [":pf:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "polynésie française", "polynesie francaise", "pf"] + }, + "flag_pg": { + "unicode": "1F1F5-1F1EC", + "unicode_alternates": [], + "name": "papua new guinea", + "shortname": ":flag_pg:", + "category": "flags", + "aliases": [":pg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "papua niu gini", "pg"] + }, + "flag_ph": { + "unicode": "1F1F5-1F1ED", + "unicode_alternates": [], + "name": "the philippines", + "shortname": ":flag_ph:", + "category": "flags", + "aliases": [":ph:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pilipinas", "ph"] + }, + "flag_pk": { + "unicode": "1F1F5-1F1F0", + "unicode_alternates": [], + "name": "pakistan", + "shortname": ":flag_pk:", + "category": "flags", + "aliases": [":pk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pk"] + }, + "flag_pl": { + "unicode": "1F1F5-1F1F1", + "unicode_alternates": [], + "name": "poland", + "shortname": ":flag_pl:", + "category": "flags", + "aliases": [":pl:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "polska", "pl"] + }, + "flag_pr": { + "unicode": "1F1F5-1F1F7", + "unicode_alternates": [], + "name": "puerto rico", + "shortname": ":flag_pr:", + "category": "flags", + "aliases": [":pr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pr"] + }, + "flag_ps": { + "unicode": "1F1F5-1F1F8", + "unicode_alternates": [], + "name": "palestinian authority", + "shortname": ":flag_ps:", + "category": "flags", + "aliases": [":ps:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ps"] + }, + "flag_pt": { + "unicode": "1F1F5-1F1F9", + "unicode_alternates": [], + "name": "portugal", + "shortname": ":flag_pt:", + "category": "flags", + "aliases": [":pt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "pt"] + }, + "flag_pw": { + "unicode": "1F1F5-1F1FC", + "unicode_alternates": [], + "name": "palau", + "shortname": ":flag_pw:", + "category": "flags", + "aliases": [":pw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "belau", "pw"] + }, + "flag_py": { + "unicode": "1F1F5-1F1FE", + "unicode_alternates": [], + "name": "paraguay", + "shortname": ":flag_py:", + "category": "flags", + "aliases": [":py:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "py"] + }, + "flag_qa": { + "unicode": "1F1F6-1F1E6", + "unicode_alternates": [], + "name": "qatar", + "shortname": ":flag_qa:", + "category": "flags", + "aliases": [":qa:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "dawlat qatar", "qa"] + }, + "flag_ro": { + "unicode": "1F1F7-1F1F4", + "unicode_alternates": [], + "name": "romania", + "shortname": ":flag_ro:", + "category": "flags", + "aliases": [":ro:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ro"] + }, + "flag_rs": { + "unicode": "1F1F7-1F1F8", + "unicode_alternates": [], + "name": "serbia", + "shortname": ":flag_rs:", + "category": "flags", + "aliases": [":rs:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "srbija", "rs"] + }, + "flag_ru": { + "unicode": "1F1F7-1F1FA", + "unicode_alternates": [], + "name": "russia", + "shortname": ":flag_ru:", + "category": "flags", + "aliases": [":ru:"], + "aliases_ascii": [], + "keywords": ["nation", "russian", "country", "ru"] + }, + "flag_rw": { + "unicode": "1F1F7-1F1FC", + "unicode_alternates": [], + "name": "rwanda", + "shortname": ":flag_rw:", + "category": "flags", + "aliases": [":rw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "rw"] + }, + "flag_sa": { + "unicode": "1F1F8-1F1E6", + "unicode_alternates": [], + "name": "saudi arabia", + "shortname": ":flag_sa:", + "category": "flags", + "aliases": [":saudiarabia:", ":saudi:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al arabiyah as suudiyah", "sa"] + }, + "flag_sb": { + "unicode": "1F1F8-1F1E7", + "unicode_alternates": [], + "name": "the solomon islands", + "shortname": ":flag_sb:", + "category": "flags", + "aliases": [":sb:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sb"] + }, + "flag_sc": { + "unicode": "1F1F8-1F1E8", + "unicode_alternates": [], + "name": "the seychelles", + "shortname": ":flag_sc:", + "category": "flags", + "aliases": [":sc:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "seychelles", "sc"] + }, + "flag_sd": { + "unicode": "1F1F8-1F1E9", + "unicode_alternates": [], + "name": "sudan", + "shortname": ":flag_sd:", + "category": "flags", + "aliases": [":sd:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "as-sudan", "sd"] + }, + "flag_se": { + "unicode": "1F1F8-1F1EA", + "unicode_alternates": [], + "name": "sweden", + "shortname": ":flag_se:", + "category": "flags", + "aliases": [":se:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sverige", "se"] + }, + "flag_sg": { + "unicode": "1F1F8-1F1EC", + "unicode_alternates": [], + "name": "singapore", + "shortname": ":flag_sg:", + "category": "flags", + "aliases": [":sg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sg"] + }, + "flag_sh": { + "unicode": "1F1F8-1F1ED", + "unicode_alternates": [], + "name": "saint helena", + "shortname": ":flag_sh:", + "category": "flags", + "aliases": [":sh:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sh"] + }, + "flag_si": { + "unicode": "1F1F8-1F1EE", + "unicode_alternates": [], + "name": "slovenia", + "shortname": ":flag_si:", + "category": "flags", + "aliases": [":si:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "slovenija", "si"] + }, + "flag_sk": { + "unicode": "1F1F8-1F1F0", + "unicode_alternates": [], + "name": "slovakia", + "shortname": ":flag_sk:", + "category": "flags", + "aliases": [":sk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sk"] + }, + "flag_sl": { + "unicode": "1F1F8-1F1F1", + "unicode_alternates": [], + "name": "sierra leone", + "shortname": ":flag_sl:", + "category": "flags", + "aliases": [":sl:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sl"] + }, + "flag_sm": { + "unicode": "1F1F8-1F1F2", + "unicode_alternates": [], + "name": "san marino", + "shortname": ":flag_sm:", + "category": "flags", + "aliases": [":sm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sm"] + }, + "flag_sn": { + "unicode": "1F1F8-1F1F3", + "unicode_alternates": [], + "name": "senegal", + "shortname": ":flag_sn:", + "category": "flags", + "aliases": [":sn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sn"] + }, + "flag_so": { + "unicode": "1F1F8-1F1F4", + "unicode_alternates": [], + "name": "somalia", + "shortname": ":flag_so:", + "category": "flags", + "aliases": [":so:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "so"] + }, + "flag_sr": { + "unicode": "1F1F8-1F1F7", + "unicode_alternates": [], + "name": "suriname", + "shortname": ":flag_sr:", + "category": "flags", + "aliases": [":sr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sr"] + }, + "flag_st": { + "unicode": "1F1F8-1F1F9", + "unicode_alternates": [], + "name": "sao tome and principe", + "shortname": ":flag_st:", + "category": "flags", + "aliases": [":st:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sao tome e principe", "st"] + }, + "flag_sv": { + "unicode": "1F1F8-1F1FB", + "unicode_alternates": [], + "name": "el salvador", + "shortname": ":flag_sv:", + "category": "flags", + "aliases": [":sv:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sv"] + }, + "flag_sy": { + "unicode": "1F1F8-1F1FE", + "unicode_alternates": [], + "name": "syria", + "shortname": ":flag_sy:", + "category": "flags", + "aliases": [":sy:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sy"] + }, + "flag_sz": { + "unicode": "1F1F8-1F1FF", + "unicode_alternates": [], + "name": "swaziland", + "shortname": ":flag_sz:", + "category": "flags", + "aliases": [":sz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "sz"] + }, + "flag_td": { + "unicode": "1F1F9-1F1E9", + "unicode_alternates": [], + "name": "chad", + "shortname": ":flag_td:", + "category": "flags", + "aliases": [":td:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tchad", "td"] + }, + "flag_tg": { + "unicode": "1F1F9-1F1EC", + "unicode_alternates": [], + "name": "togo", + "shortname": ":flag_tg:", + "category": "flags", + "aliases": [":tg:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "republique togolaise", "tg"] + }, + "flag_th": { + "unicode": "1F1F9-1F1ED", + "unicode_alternates": [], + "name": "thailand", + "shortname": ":flag_th:", + "category": "flags", + "aliases": [":th:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "prathet thai", "th"] + }, + "flag_tj": { + "unicode": "1F1F9-1F1EF", + "unicode_alternates": [], + "name": "tajikistan", + "shortname": ":flag_tj:", + "category": "flags", + "aliases": [":tj:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "jumhurii tojikiston", "tj"] + }, + "flag_tl": { + "unicode": "1F1F9-1F1F1", + "unicode_alternates": [], + "name": "east timor", + "shortname": ":flag_tl:", + "category": "flags", + "aliases": [":tl:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tl"] + }, + "flag_tm": { + "unicode": "1F1F9-1F1F2", + "unicode_alternates": [], + "name": "turkmenistan", + "shortname": ":flag_tm:", + "category": "flags", + "aliases": [":turkmenistan:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tm"] + }, + "flag_tn": { + "unicode": "1F1F9-1F1F3", + "unicode_alternates": [], + "name": "tunisia", + "shortname": ":flag_tn:", + "category": "flags", + "aliases": [":tn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tunis", "tn"] + }, + "flag_to": { + "unicode": "1F1F9-1F1F4", + "unicode_alternates": [], + "name": "tonga", + "shortname": ":flag_to:", + "category": "flags", + "aliases": [":to:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "to"] + }, + "flag_tr": { + "unicode": "1F1F9-1F1F7", + "unicode_alternates": [], + "name": "turkey", + "shortname": ":flag_tr:", + "category": "flags", + "aliases": [":tr:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "turkiye"] + }, + "flag_tt": { + "unicode": "1F1F9-1F1F9", + "unicode_alternates": [], + "name": "trinidad and tobago", + "shortname": ":flag_tt:", + "category": "flags", + "aliases": [":tt:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tt"] + }, + "flag_tv": { + "unicode": "1F1F9-1F1FB", + "unicode_alternates": [], + "name": "tuvalu", + "shortname": ":flag_tv:", + "category": "flags", + "aliases": [":tuvalu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tv"] + }, + "flag_tw": { + "unicode": "1F1F9-1F1FC", + "unicode_alternates": [], + "name": "the republic of china", + "shortname": ":flag_tw:", + "category": "flags", + "aliases": [":tw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "taiwan", "tw"] + }, + "flag_tz": { + "unicode": "1F1F9-1F1FF", + "unicode_alternates": [], + "name": "tanzania", + "shortname": ":flag_tz:", + "category": "flags", + "aliases": [":tz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "tz"] + }, + "flag_ua": { + "unicode": "1F1FA-1F1E6", + "unicode_alternates": [], + "name": "ukraine", + "shortname": ":flag_ua:", + "category": "flags", + "aliases": [":ua:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ukrayina", "ua"] + }, + "flag_ug": { + "unicode": "1F1FA-1F1EC", + "unicode_alternates": [], + "name": "uganda", + "shortname": ":flag_ug:", + "category": "flags", + "aliases": [":ug:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ug"] + }, + "flag_us": { + "unicode": "1F1FA-1F1F8", + "unicode_alternates": [], + "name": "united states", + "shortname": ":flag_us:", + "category": "flags", + "aliases": [":us:"], + "aliases_ascii": [], + "keywords": ["american", "country", "nation", "usa", "united states of america", "america", "old glory", "us"] + }, + "flag_uy": { + "unicode": "1F1FA-1F1FE", + "unicode_alternates": [], + "name": "uruguay", + "shortname": ":flag_uy:", + "category": "flags", + "aliases": [":uy:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "uy"] + }, + "flag_uz": { + "unicode": "1F1FA-1F1FF", + "unicode_alternates": [], + "name": "uzbekistan", + "shortname": ":flag_uz:", + "category": "flags", + "aliases": [":uz:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "uzbekiston respublikasi", "uz"] + }, + "flag_va": { + "unicode": "1F1FB-1F1E6", + "unicode_alternates": [], + "name": "the vatican city", + "shortname": ":flag_va:", + "category": "flags", + "aliases": [":va:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "va"] + }, + "flag_vc": { + "unicode": "1F1FB-1F1E8", + "unicode_alternates": [], + "name": "saint vincent and the grenadines", + "shortname": ":flag_vc:", + "category": "flags", + "aliases": [":vc:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "vc"] + }, + "flag_ve": { + "unicode": "1F1FB-1F1EA", + "unicode_alternates": [], + "name": "venezuela", + "shortname": ":flag_ve:", + "category": "flags", + "aliases": [":ve:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "ve"] + }, + "flag_vi": { + "unicode": "1F1FB-1F1EE", + "unicode_alternates": [], + "name": "u.s. virgin islands", + "shortname": ":flag_vi:", + "category": "flags", + "aliases": [":vi:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "vi"] + }, + "flag_vn": { + "unicode": "1F1FB-1F1F3", + "unicode_alternates": [], + "name": "vietnam", + "shortname": ":flag_vn:", + "category": "flags", + "aliases": [":vn:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "viet nam", "vn"] + }, + "flag_vu": { + "unicode": "1F1FB-1F1FA", + "unicode_alternates": [], + "name": "vanuatu", + "shortname": ":flag_vu:", + "category": "flags", + "aliases": [":vu:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "vu"] + }, + "flag_wf": { + "unicode": "1F1FC-1F1EB", + "unicode_alternates": [], + "name": "wallis and futuna", + "shortname": ":flag_wf:", + "category": "flags", + "aliases": [":wf:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "wf"] + }, + "flag_white": { + "unicode": "1F3F3", + "unicode_alternates": [], + "name": "waving white flag", + "shortname": ":flag_white:", + "category": "objects_symbols", + "aliases": [":waving_white_flag:"], + "aliases_ascii": [], + "keywords": ["symbol", "signal"] + }, + "flag_ws": { + "unicode": "1F1FC-1F1F8", + "unicode_alternates": [], + "name": "samoa", + "shortname": ":flag_ws:", + "category": "flags", + "aliases": [":ws:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "american samoa", "ws"] + }, + "flag_xk": { + "unicode": "1F1FD-1F1F0", + "unicode_alternates": [], + "name": "kosovo", + "shortname": ":flag_xk:", + "category": "flags", + "aliases": [":xk:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "xk"] + }, + "flag_ye": { + "unicode": "1F1FE-1F1EA", + "unicode_alternates": [], + "name": "yemen", + "shortname": ":flag_ye:", + "category": "flags", + "aliases": [":ye:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "al yaman", "ye"] + }, + "flag_za": { + "unicode": "1F1FF-1F1E6", + "unicode_alternates": [], + "name": "south africa", + "shortname": ":flag_za:", + "category": "flags", + "aliases": [":za:"], + "aliases_ascii": [], + "keywords": ["country", "nation"] + }, + "flag_zm": { + "unicode": "1F1FF-1F1F2", + "unicode_alternates": [], + "name": "zambia", + "shortname": ":flag_zm:", + "category": "flags", + "aliases": [":zm:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "zm"] + }, + "flag_zw": { + "unicode": "1F1FF-1F1FC", + "unicode_alternates": [], + "name": "zimbabwe", + "shortname": ":flag_zw:", + "category": "flags", + "aliases": [":zw:"], + "aliases_ascii": [], + "keywords": ["country", "nation", "zw"] + }, + "flags": { + "unicode": "1F38F", + "unicode_alternates": [], + "name": "carp streamer", + "shortname": ":flags:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["banner", "carp", "fish", "japanese", "koinobori", "children", "kids", "boys", "celebration", "happiness", "carp", "streamers", "japanese", "holiday", "flags"], + "moji": "ðŸŽ" + }, + "flashlight": { + "unicode": "1F526", + "unicode_alternates": [], + "name": "electric torch", + "shortname": ":flashlight:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dark"], + "moji": "🔦" + }, + "flip_phone": { + "unicode": "1F581", + "unicode_alternates": [], + "name": "clamshell mobile phone", + "shortname": ":flip_phone:", + "category": "objects_symbols", + "aliases": [":clamshell_mobile_phone:"], + "aliases_ascii": [], + "keywords": ["cellphone"] + }, + "floppy_black": { + "unicode": "1F5AA", + "unicode_alternates": [], + "name": "black hard shell floppy disk", + "shortname": ":floppy_black:", + "category": "objects_symbols", + "aliases": [":black_hard_shell_floppy_disk:"], + "aliases_ascii": [], + "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"] + }, + "floppy_disk": { + "unicode": "1F4BE", + "unicode_alternates": [], + "name": "floppy disk", + "shortname": ":floppy_disk:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["oldschool", "save", "technology", "floppy", "disk", "storage", "information", "computer", "drive", "megabyte"], + "moji": "💾" + }, + "floppy_white": { + "unicode": "1F5AB", + "unicode_alternates": [], + "name": "white hard shell floppy disk", + "shortname": ":floppy_white:", + "category": "objects_symbols", + "aliases": [":white_hard_shell_floppy_disk:"], + "aliases_ascii": [], + "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"] + }, + "flower_playing_cards": { + "unicode": "1F3B4", + "unicode_alternates": [], + "name": "flower playing cards", + "shortname": ":flower_playing_cards:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["playing", "card", "flower", "game", "august", "moon", "special"], + "moji": "🎴" + }, + "flushed": { + "unicode": "1F633", + "unicode_alternates": [], + "name": "flushed face", + "shortname": ":flushed:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":$", "=$"], + "keywords": ["blush", "face", "flattered", "flush", "blush", "red", "pink", "cheeks", "shy"], + "moji": "😳" + }, + "fog": { + "unicode": "1F32B", + "unicode_alternates": [], + "name": "fog", + "shortname": ":fog:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["weather", "damp", "cloud", "hazy"] + }, + "foggy": { + "unicode": "1F301", + "unicode_alternates": [], + "name": "foggy", + "shortname": ":foggy:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mountain", "photo", "bridge", "weather", "fog", "foggy"], + "moji": "ðŸŒ" + }, + "folder": { + "unicode": "1F5C0", + "unicode_alternates": [], + "name": "folder", + "shortname": ":folder:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"] + }, + "folder_open": { + "unicode": "1F5C1", + "unicode_alternates": [], + "name": "open folder", + "shortname": ":folder_open:", + "category": "objects_symbols", + "aliases": [":open_folder:"], + "aliases_ascii": [], + "keywords": ["documents", "load"] + }, + "football": { + "unicode": "1F3C8", + "unicode_alternates": [], + "name": "american football", + "shortname": ":football:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["NFL", "balls", "sports", "football", "ball", "sport", "america", "american"], + "moji": "ðŸˆ" + }, + "footprints": { + "unicode": "1F463", + "unicode_alternates": [], + "name": "footprints", + "shortname": ":footprints:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["feet"], + "moji": "👣" + }, + "fork_and_knife": { + "unicode": "1F374", + "unicode_alternates": [], + "name": "fork and knife", + "shortname": ":fork_and_knife:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cutlery", "kitchen", "fork", "knife", "restaurant", "meal", "food", "eat"], + "moji": "ðŸ´" + }, + "fork_knife_plate": { + "unicode": "1F37D", + "unicode_alternates": [], + "name": "fork and knife with plate", + "shortname": ":fork_knife_plate:", + "category": "travel_places", + "aliases": [":fork_and_knife_with_plate:"], + "aliases_ascii": [], + "keywords": ["meal", "food", "breakfast", "lunch", "dinner", "utensils", "setting"] + }, + "fountain": { + "unicode": "26F2", + "unicode_alternates": ["26F2-FE0F"], + "name": "fountain", + "shortname": ":fountain:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo"], + "moji": "⛲" + }, + "four": { + "moji": "4ï¸âƒ£", + "unicode": "0034-20E3", + "unicode_alternates": ["0034-FE0F-20E3"], + "name": "digit four", + "shortname": ":four:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["4", "blue-square", "numbers"] + }, + "four_leaf_clover": { + "unicode": "1F340", + "unicode_alternates": [], + "name": "four leaf clover", + "shortname": ":four_leaf_clover:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["lucky", "nature", "plant", "vegetable", "clover", "four", "leaf", "luck", "irish", "saint", "patrick", "green"], + "moji": "ðŸ€" + }, + "frame_photo": { + "unicode": "1F5BC", + "unicode_alternates": [], + "name": "frame with picture", + "shortname": ":frame_photo:", + "category": "objects_symbols", + "aliases": [":frame_with_picture:"], + "aliases_ascii": [], + "keywords": ["photo"] + }, + "frame_tiles": { + "unicode": "1F5BD", + "unicode_alternates": [], + "name": "frame with tiles", + "shortname": ":frame_tiles:", + "category": "objects_symbols", + "aliases": [":frame_with_tiles:"], + "aliases_ascii": [], + "keywords": ["photo", "painting"] + }, + "frame_x": { + "unicode": "1F5BE", + "unicode_alternates": [], + "name": "frame with an x", + "shortname": ":frame_x:", + "category": "objects_symbols", + "aliases": [":frame_with_an_x:"], + "aliases_ascii": [], + "keywords": ["photo", "painting"] + }, + "free": { + "unicode": "1F193", + "unicode_alternates": [], + "name": "squared free", + "shortname": ":free:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "words"], + "moji": "🆓" + }, + "fried_shrimp": { + "unicode": "1F364", + "unicode_alternates": [], + "name": "fried shrimp", + "shortname": ":fried_shrimp:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "food", "shrimp", "fried", "seafood", "small", "fish"], + "moji": "ðŸ¤" + }, + "fries": { + "unicode": "1F35F", + "unicode_alternates": [], + "name": "french fries", + "shortname": ":fries:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chips", "food", "fries", "french", "potato", "fry", "russet", "idaho"], + "moji": "ðŸŸ" + }, + "frog": { + "unicode": "1F438", + "unicode_alternates": [], + "name": "frog face", + "shortname": ":frog:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ¸" + }, + "frowning": { + "unicode": "1F626", + "unicode_alternates": [], + "name": "frowning face with open mouth", + "shortname": ":frowning:", + "category": "emoticons", + "aliases": [":anguished:"], + "aliases_ascii": [], + "keywords": ["aw", "face", "frown", "sad", "pout", "sulk", "glower"], + "moji": "😦" + }, + "fuelpump": { + "unicode": "26FD", + "unicode_alternates": ["26FD-FE0F"], + "name": "fuel pump", + "shortname": ":fuelpump:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gas station", "petroleum"], + "moji": "⛽" + }, + "full_moon": { + "unicode": "1F315", + "unicode_alternates": [], + "name": "full moon symbol", + "shortname": ":full_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "yellow", "moon", "full", "sky", "night", "cheese", "phase", "monster", "spooky", "werewolves", "twilight"], + "moji": "🌕" + }, + "full_moon_with_face": { + "unicode": "1F31D", + "unicode_alternates": [], + "name": "full moon with face", + "shortname": ":full_moon_with_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "moon", "full", "anthropomorphic", "face", "sky", "night", "cheese", "phase", "spooky", "werewolves", "monsters"], + "moji": "ðŸŒ" + }, + "game_die": { + "unicode": "1F3B2", + "unicode_alternates": [], + "name": "game die", + "shortname": ":game_die:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dice", "game", "die", "dice", "craps", "gamble", "play"], + "moji": "🎲" + }, + "gem": { + "unicode": "1F48E", + "unicode_alternates": [], + "name": "gem stone", + "shortname": ":gem:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue", "ruby"], + "moji": "💎" + }, + "gemini": { + "unicode": "264A", + "unicode_alternates": ["264A-FE0F"], + "name": "gemini", + "shortname": ":gemini:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gemini", "twins", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "moji": "♊" + }, + "ghost": { + "unicode": "1F47B", + "unicode_alternates": [], + "name": "ghost", + "shortname": ":ghost:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["halloween"], + "moji": "👻" + }, + "gift": { + "unicode": "1F381", + "unicode_alternates": [], + "name": "wrapped present", + "shortname": ":gift:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["birthday", "christmas", "present", "xmas", "gift", "present", "wrap", "package", "birthday", "wedding"], + "moji": "ðŸŽ" + }, + "gift_heart": { + "unicode": "1F49D", + "unicode_alternates": [], + "name": "heart with ribbon", + "shortname": ":gift_heart:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["love", "valentines"], + "moji": "ðŸ’" + }, + "girl": { + "unicode": "1F467", + "unicode_alternates": [], + "name": "girl", + "shortname": ":girl:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "woman"], + "moji": "👧" + }, + "girls_symbol": { + "unicode": "1F6CA", + "unicode_alternates": [], + "name": "girls symbol", + "shortname": ":girls_symbol:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "child"] + }, + "globe_with_meridians": { + "unicode": "1F310", + "unicode_alternates": [], + "name": "globe with meridians", + "shortname": ":globe_with_meridians:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["earth", "international", "world", "earth", "meridian", "globe", "space", "planet", "home"], + "moji": "ðŸŒ" + }, + "goat": { + "unicode": "1F410", + "unicode_alternates": [], + "name": "goat", + "shortname": ":goat:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "goat", "sheep", "kid", "billy", "livestock"], + "moji": "ðŸ" + }, + "golf": { + "unicode": "26F3", + "unicode_alternates": ["26F3-FE0F"], + "name": "flag in hole", + "shortname": ":golf:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["business", "sports"], + "moji": "⛳" + }, + "golfer": { + "unicode": "1F3CC", + "unicode_alternates": [], + "name": "golfer", + "shortname": ":golfer:", + "category": "activity", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sport", "par", "birdie", "eagle", "mulligan"] + }, + "grapes": { + "unicode": "1F347", + "unicode_alternates": [], + "name": "grapes", + "shortname": ":grapes:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "grapes", "wine", "vinegar", "fruit", "cluster", "vine"], + "moji": "ðŸ‡" + }, + "green_apple": { + "unicode": "1F34F", + "unicode_alternates": [], + "name": "green apple", + "shortname": ":green_apple:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fruit", "nature", "apple", "fruit", "green", "pie", "granny", "smith", "core"], + "moji": "ðŸ" + }, + "green_book": { + "unicode": "1F4D7", + "unicode_alternates": [], + "name": "green book", + "shortname": ":green_book:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["knowledge", "library", "read"], + "moji": "📗" + }, + "green_heart": { + "unicode": "1F49A", + "unicode_alternates": [], + "name": "green heart", + "shortname": ":green_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "green", "heart", "love", "nature", "rebirth", "reborn", "jealous", "clingy", "envious", "possessive"], + "moji": "💚" + }, + "grey_exclamation": { + "unicode": "2755", + "unicode_alternates": [], + "name": "white exclamation mark ornament", + "shortname": ":grey_exclamation:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["surprise"], + "moji": "â•" + }, + "grey_question": { + "unicode": "2754", + "unicode_alternates": [], + "name": "white question mark ornament", + "shortname": ":grey_question:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["doubts"], + "moji": "â”" + }, + "grimacing": { + "unicode": "1F62C", + "unicode_alternates": [], + "name": "grimacing face", + "shortname": ":grimacing:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "grimace", "teeth", "grimace", "disapprove", "pain"], + "moji": "😬" + }, + "grin": { + "unicode": "1F601", + "unicode_alternates": [], + "name": "grinning face with smiling eyes", + "shortname": ":grin:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"], + "moji": "ðŸ˜" + }, + "grinning": { + "unicode": "1F600", + "unicode_alternates": [], + "name": "grinning face", + "shortname": ":grinning:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"], + "moji": "🕧" + }, + "guardsman": { + "unicode": "1F482", + "unicode_alternates": [], + "name": "guardsman", + "shortname": ":guardsman:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["british", "gb", "male", "man", "uk", "guardsman", "guard", "bearskin", "hat", "british", "queen", "ceremonial", "military"], + "moji": "💂" + }, + "guitar": { + "unicode": "1F3B8", + "unicode_alternates": [], + "name": "guitar", + "shortname": ":guitar:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["instrument", "music", "guitar", "string", "music", "instrument", "jam", "rock", "acoustic", "electric"], + "moji": "🎸" + }, + "gun": { + "unicode": "1F52B", + "unicode_alternates": [], + "name": "pistol", + "shortname": ":gun:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["violence", "weapon"], + "moji": "🔫" + }, + "haircut": { + "unicode": "1F487", + "unicode_alternates": [], + "name": "haircut", + "shortname": ":haircut:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman"], + "moji": "💇" + }, + "hamburger": { + "unicode": "1F354", + "unicode_alternates": [], + "name": "hamburger", + "shortname": ":hamburger:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "meat", "hamburger", "burger", "meat", "cow", "beef"], + "moji": "ðŸ”" + }, + "hammer": { + "unicode": "1F528", + "unicode_alternates": [], + "name": "hammer", + "shortname": ":hammer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["done", "judge", "law", "ruling", "tools", "verdict"], + "moji": "🔨" + }, + "hamster": { + "unicode": "1F439", + "unicode_alternates": [], + "name": "hamster face", + "shortname": ":hamster:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ¹" + }, + "hand_splayed": { + "unicode": "1F590", + "unicode_alternates": [], + "name": "raised hand with fingers splayed", + "shortname": ":hand_splayed:", + "category": "people", + "aliases": [":raised_hand_with_fingers_splayed:"], + "aliases_ascii": [], + "keywords": ["hi", "five", "stop", "halt"] + }, + "hand_splayed_reverse": { + "unicode": "1F591", + "unicode_alternates": [], + "name": "reversed raised hand with fingers splayed", + "shortname": ":hand_splayed_reverse:", + "category": "people", + "aliases": [":reversed_raised_hand_with_fingers_splayed:"], + "aliases_ascii": [], + "keywords": ["hi", "five", "stop", "halt"] + }, + "hand_victory": { + "unicode": "1F594", + "unicode_alternates": [], + "name": "reversed victory hand", + "shortname": ":hand_victory:", + "category": "people", + "aliases": [":reversed_victory_hand:"], + "aliases_ascii": [], + "keywords": ["fu"] + }, + "handbag": { + "unicode": "1F45C", + "unicode_alternates": [], + "name": "handbag", + "shortname": ":handbag:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "accessory", "bag", "fashion"], + "moji": "👜" + }, + "hard_disk": { + "unicode": "1F5B4", + "unicode_alternates": [], + "name": "hard disk", + "shortname": ":hard_disk:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["save", "technology", "storage", "information", "computer", "drive", "megabyte", "gigabyte", "hd"] + }, + "hash": { + "moji": "#⃣", + "unicode": "0023-20E3", + "unicode_alternates": ["0023-FE0F-20E3"], + "name": "number sign", + "shortname": ":hash:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["symbol"] + }, + "hatched_chick": { + "unicode": "1F425", + "unicode_alternates": [], + "name": "front-facing baby chick", + "shortname": ":hatched_chick:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["baby", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"], + "moji": "ðŸ¥" + }, + "hatching_chick": { + "unicode": "1F423", + "unicode_alternates": [], + "name": "hatching chick", + "shortname": ":hatching_chick:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["born", "chicken", "egg", "chick", "egg", "baby", "bird", "chicken", "young", "woman", "cute"], + "moji": "ðŸ£" + }, + "headphones": { + "unicode": "1F3A7", + "unicode_alternates": [], + "name": "headphone", + "shortname": ":headphones:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gadgets", "music", "score", "headphone", "sound", "music", "ears", "beats", "buds", "audio", "listen"], + "moji": "🎧" + }, + "hear_no_evil": { + "unicode": "1F649", + "unicode_alternates": [], + "name": "hear-no-evil monkey", + "shortname": ":hear_no_evil:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "monkey", "monkey", "ears", "hear", "sound", "kikazaru"], + "moji": "🙉" + }, + "heart": { + "moji": "â¤", + "unicode": "2764", + "unicode_alternates": ["2764-FE0F"], + "name": "heavy black heart", + "shortname": ":heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["<3"], + "keywords": ["like", "love", "red", "pink", "black", "heart", "love", "passion", "romance", "intense", "desire", "death", "evil", "cold", "valentines"] + }, + "heart_decoration": { + "unicode": "1F49F", + "unicode_alternates": [], + "name": "heart decoration", + "shortname": ":heart_decoration:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["like", "love", "purple-square"], + "moji": "💟" + }, + "heart_eyes": { + "unicode": "1F60D", + "unicode_alternates": [], + "name": "smiling face with heart-shaped eyes", + "shortname": ":heart_eyes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "crush", "face", "infatuation", "like", "love", "valentines", "smiling", "heart", "lovestruck", "love", "flirt", "smile", "heart-shaped"], + "moji": "ðŸ˜" + }, + "heart_eyes_cat": { + "unicode": "1F63B", + "unicode_alternates": [], + "name": "smiling cat face with heart-shaped eyes", + "shortname": ":heart_eyes_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "animal", "cats", "like", "love", "valentines", "lovestruck", "love", "heart"], + "moji": "😻" + }, + "heart_tip": { + "unicode": "1F394", + "unicode_alternates": [], + "name": "heart with tip on the left", + "shortname": ":heart_tip:", + "category": "celebration", + "aliases": [":heart_with_tip_on_the_left:"], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines"] + }, + "heartbeat": { + "unicode": "1F493", + "unicode_alternates": [], + "name": "beating heart", + "shortname": ":heartbeat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines"], + "moji": "💓" + }, + "heartpulse": { + "unicode": "1F497", + "unicode_alternates": [], + "name": "growing heart", + "shortname": ":heartpulse:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines"], + "moji": "💗" + }, + "hearts": { + "unicode": "2665", + "unicode_alternates": ["2665-FE0F"], + "name": "black heart suit", + "shortname": ":hearts:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cards", "poker"], + "moji": "♥" + }, + "heavy_check_mark": { + "unicode": "2714", + "unicode_alternates": ["2714-FE0F"], + "name": "heavy check mark", + "shortname": ":heavy_check_mark:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nike", "ok"], + "moji": "✔" + }, + "heavy_division_sign": { + "unicode": "2797", + "unicode_alternates": [], + "name": "heavy division sign", + "shortname": ":heavy_division_sign:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["calculation", "divide", "math"], + "moji": "âž—" + }, + "heavy_dollar_sign": { + "unicode": "1F4B2", + "unicode_alternates": [], + "name": "heavy dollar sign", + "shortname": ":heavy_dollar_sign:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["currency", "money", "payment", "dollar", "currency", "money", "cash", "sale", "purchase", "value"], + "moji": "💲" + }, + "heavy_minus_sign": { + "unicode": "2796", + "unicode_alternates": [], + "name": "heavy minus sign", + "shortname": ":heavy_minus_sign:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["calculation", "math"], + "moji": "âž–" + }, + "heavy_multiplication_x": { + "unicode": "2716", + "unicode_alternates": ["2716-FE0F"], + "name": "heavy multiplication x", + "shortname": ":heavy_multiplication_x:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["calculation", "math"], + "moji": "✖" + }, + "heavy_plus_sign": { + "unicode": "2795", + "unicode_alternates": [], + "name": "heavy plus sign", + "shortname": ":heavy_plus_sign:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["calculation", "math"], + "moji": "âž•" + }, + "helicopter": { + "unicode": "1F681", + "unicode_alternates": [], + "name": "helicopter", + "shortname": ":helicopter:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "helicopter", "helo", "gyro", "gyrocopter"], + "moji": "ðŸš" + }, + "herb": { + "unicode": "1F33F", + "unicode_alternates": [], + "name": "herb", + "shortname": ":herb:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["grass", "lawn", "medicine", "plant", "vegetable", "weed", "herb", "spice", "plant", "cook", "cooking"], + "moji": "🌿" + }, + "hibiscus": { + "unicode": "1F33A", + "unicode_alternates": [], + "name": "hibiscus", + "shortname": ":hibiscus:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flowers", "plant", "vegetable", "hibiscus", "flower", "warm"], + "moji": "🌺" + }, + "high_brightness": { + "unicode": "1F506", + "unicode_alternates": [], + "name": "high brightness symbol", + "shortname": ":high_brightness:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["light", "summer", "sun"], + "moji": "🔆" + }, + "high_heel": { + "unicode": "1F460", + "unicode_alternates": [], + "name": "high-heeled shoe", + "shortname": ":high_heel:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "female", "shoes"], + "moji": "👠" + }, + "hole": { + "unicode": "1F573", + "unicode_alternates": [], + "name": "hole", + "shortname": ":hole:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["pit", "well"] + }, + "homes": { + "unicode": "1F3D8", + "unicode_alternates": [], + "name": "house buildings", + "shortname": ":homes:", + "category": "travel_places", + "aliases": [":house_buildings:"], + "aliases_ascii": [], + "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"] + }, + "honey_pot": { + "unicode": "1F36F", + "unicode_alternates": [], + "name": "honey pot", + "shortname": ":honey_pot:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bees", "sweet", "honey", "pot", "bees", "pooh", "bear"], + "moji": "ðŸ¯" + }, + "horse": { + "unicode": "1F434", + "unicode_alternates": [], + "name": "horse face", + "shortname": ":horse:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "brown"], + "moji": "ðŸ´" + }, + "horse_racing": { + "unicode": "1F3C7", + "unicode_alternates": [], + "name": "horse racing", + "shortname": ":horse_racing:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "betting", "competition", "horse", "race", "racing", "jockey", "triple crown"], + "moji": "ðŸ‡" + }, + "hospital": { + "unicode": "1F3E5", + "unicode_alternates": [], + "name": "hospital", + "shortname": ":hospital:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "doctor", "health", "surgery"], + "moji": "ðŸ¥" + }, + "hot_pepper": { + "unicode": "1F336", + "unicode_alternates": [], + "name": "hot pepper", + "shortname": ":hot_pepper:", + "category": "food_drink", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "nature", "spicy", "chili", "cayenne", "habanero", "jalapeno"] + }, + "hotel": { + "unicode": "1F3E8", + "unicode_alternates": [], + "name": "hotel", + "shortname": ":hotel:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accomodation", "building", "checkin", "whotel", "hotel", "motel", "holiday inn", "hospital"], + "moji": "ðŸ¨" + }, + "hotsprings": { + "unicode": "2668", + "unicode_alternates": ["2668-FE0F"], + "name": "hot springs", + "shortname": ":hotsprings:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bath", "relax", "warm"], + "moji": "♨" + }, + "hourglass": { + "unicode": "231B", + "unicode_alternates": ["231B-FE0F"], + "name": "hourglass", + "shortname": ":hourglass:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clock", "oldschool", "time"], + "moji": "⌛" + }, + "hourglass_flowing_sand": { + "unicode": "23F3", + "unicode_alternates": [], + "name": "hourglass with flowing sand", + "shortname": ":hourglass_flowing_sand:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["countdown", "oldschool", "time"], + "moji": "â³" + }, + "house": { + "unicode": "1F3E0", + "unicode_alternates": [], + "name": "house building", + "shortname": ":house:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "home", "house", "home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"], + "moji": "ðŸ " + }, + "house_abandoned": { + "unicode": "1F3DA", + "unicode_alternates": [], + "name": "derelict house building", + "shortname": ":house_abandoned:", + "category": "travel_places", + "aliases": [":derelict_house_building:"], + "aliases_ascii": [], + "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman", "boarded", "abandoned", "vacant", "run down", "shoddy"] + }, + "house_with_garden": { + "unicode": "1F3E1", + "unicode_alternates": [], + "name": "house with garden", + "shortname": ":house_with_garden:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["home", "nature", "plant"], + "moji": "ðŸ¡" + }, + "hushed": { + "unicode": "1F62F", + "unicode_alternates": [], + "name": "hushed face", + "shortname": ":hushed:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "woo", "quiet", "hush", "whisper", "silent"], + "moji": "😯" + }, + "ice_cream": { + "unicode": "1F368", + "unicode_alternates": [], + "name": "ice cream", + "shortname": ":ice_cream:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "waffle"], + "moji": "ðŸ¨" + }, + "icecream": { + "unicode": "1F366", + "unicode_alternates": [], + "name": "soft ice cream", + "shortname": ":icecream:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "yogurt"], + "moji": "ðŸ¦" + }, + "ideograph_advantage": { + "unicode": "1F250", + "unicode_alternates": [], + "name": "circled ideograph advantage", + "shortname": ":ideograph_advantage:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "get", "kanji", "obtain"], + "moji": "ðŸ‰" + }, + "imp": { + "unicode": "1F47F", + "unicode_alternates": [], + "name": "imp", + "shortname": ":imp:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["angry", "devil", "evil", "horns", "cute", "devil"], + "moji": "👿" + }, + "inbox_tray": { + "unicode": "1F4E5", + "unicode_alternates": [], + "name": "inbox tray", + "shortname": ":inbox_tray:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents", "email"], + "moji": "📥" + }, + "incoming_envelope": { + "unicode": "1F4E8", + "unicode_alternates": [], + "name": "incoming envelope", + "shortname": ":incoming_envelope:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["email", "inbox"], + "moji": "📨" + }, + "info": { + "unicode": "1F6C8", + "unicode_alternates": [], + "name": "circled information source", + "shortname": ":info:", + "category": "objects_symbols", + "aliases": [":circled_information_source:"], + "aliases_ascii": [], + "keywords": ["icon"] + }, + "information_desk_person": { + "unicode": "1F481", + "unicode_alternates": [], + "name": "information desk person", + "shortname": ":information_desk_person:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "human", "woman", "information", "help", "question", "answer", "sassy", "unimpressed", "attitude", "snarky"], + "moji": "ðŸ’" + }, + "information_source": { + "unicode": "2139", + "unicode_alternates": ["2139-FE0F"], + "name": "information source", + "shortname": ":information_source:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-square", "letter"], + "moji": "ℹ" + }, + "innocent": { + "unicode": "1F607", + "unicode_alternates": [], + "name": "smiling face with halo", + "shortname": ":innocent:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["O:-)", "0:-3", "0:3", "0:-)", "0:)", "0;^)", "O:-)", "O:)", "O;-)", "O=)", "0;-)", "O:-3", "O:3"], + "keywords": ["angel", "face", "halo", "halo", "angel", "innocent", "ring", "circle", "heaven"], + "moji": "😇" + }, + "interrobang": { + "unicode": "2049", + "unicode_alternates": ["2049-FE0F"], + "name": "exclamation question mark", + "shortname": ":interrobang:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["punctuation", "surprise", "wat"], + "moji": "â‰" + }, + "iphone": { + "unicode": "1F4F1", + "unicode_alternates": [], + "name": "mobile phone", + "shortname": ":iphone:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["apple", "dial", "gadgets", "technology"], + "moji": "📱" + }, + "island": { + "unicode": "1F3DD", + "unicode_alternates": [], + "name": "desert island", + "shortname": ":island:", + "category": "travel_places", + "aliases": [":desert_island:"], + "aliases_ascii": [], + "keywords": ["land", "solitude", "alone"] + }, + "izakaya_lantern": { + "unicode": "1F3EE", + "unicode_alternates": [], + "name": "izakaya lantern", + "shortname": ":izakaya_lantern:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["light", "izakaya", "lantern", "stay", "drink", "alcohol", "bar", "sake", "restaurant"], + "moji": "ðŸ®" + }, + "jack_o_lantern": { + "unicode": "1F383", + "unicode_alternates": [], + "name": "jack-o-lantern", + "shortname": ":jack_o_lantern:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["halloween", "jack-o-lantern", "pumpkin", "halloween", "holiday", "carve", "autumn", "fall", "october", "saints", "costume", "spooky", "horror", "scary", "scared", "dead"], + "moji": "🎃" + }, + "japan": { + "unicode": "1F5FE", + "unicode_alternates": [], + "name": "silhouette of japan", + "shortname": ":japan:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nation"], + "moji": "🗾" + }, + "japanese_castle": { + "unicode": "1F3EF", + "unicode_alternates": [], + "name": "japanese castle", + "shortname": ":japanese_castle:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "photo", "castle", "japanese", "residence", "royalty", "fort", "fortified", "fortress"], + "moji": "ðŸ¯" + }, + "japanese_goblin": { + "unicode": "1F47A", + "unicode_alternates": [], + "name": "japanese goblin", + "shortname": ":japanese_goblin:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["evil", "mask", "red", "japanese", "tengu", "supernatural", "avian", "demon", "goblin", "mask", "theater", "nose", "frown", "mustache", "anger", "frustration"], + "moji": "👺" + }, + "japanese_ogre": { + "unicode": "1F479", + "unicode_alternates": [], + "name": "japanese ogre", + "shortname": ":japanese_ogre:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["monster", "japanese", "oni", "demon", "troll", "ogre", "folklore", "monster", "devil", "mask", "theater", "horns", "teeth"], + "moji": "👹" + }, + "jeans": { + "unicode": "1F456", + "unicode_alternates": [], + "name": "jeans", + "shortname": ":jeans:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "shopping", "jeans", "pants", "blue", "denim", "levi's", "levi", "designer", "work", "skinny"], + "moji": "👖" + }, + "jet_up": { + "unicode": "1F6E6", + "unicode_alternates": [], + "name": "up-pointing military airplane", + "shortname": ":jet_up:", + "category": "travel_places", + "aliases": [":up_pointing_military_airplane:"], + "aliases_ascii": [], + "keywords": ["jet"] + }, + "joy": { + "unicode": "1F602", + "unicode_alternates": [], + "name": "face with tears of joy", + "shortname": ":joy:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":')", ":'-)"], + "keywords": ["cry", "face", "haha", "happy", "tears", "tears", "cry", "joy", "happy", "weep"], + "moji": "😂" + }, + "joy_cat": { + "unicode": "1F639", + "unicode_alternates": [], + "name": "cat face with tears of joy", + "shortname": ":joy_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "haha", "happy", "tears", "happy", "tears", "cry", "joy"], + "moji": "😹" + }, + "joystick": { + "unicode": "1F579", + "unicode_alternates": [], + "name": "joystick", + "shortname": ":joystick:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["games", "atari", "controller"] + }, + "key": { + "unicode": "1F511", + "unicode_alternates": [], + "name": "key", + "shortname": ":key:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["door", "lock", "password"], + "moji": "🔑" + }, + "key2": { + "unicode": "1F5DD", + "unicode_alternates": [], + "name": "old key", + "shortname": ":key2:", + "category": "objects_symbols", + "aliases": [":old_key:"], + "aliases_ascii": [], + "keywords": ["door", "lock", "password", "skeleton"] + }, + "keyboard": { + "unicode": "1F5AE", + "unicode_alternates": [], + "name": "wired keyboard", + "shortname": ":keyboard:", + "category": "objects_symbols", + "aliases": [":wired_keyboard:"], + "aliases_ascii": [], + "keywords": ["typing", "keys", "input", "device"] + }, + "keyboard_mouse": { + "unicode": "1F5A6", + "unicode_alternates": [], + "name": "keyboard and mouse", + "shortname": ":keyboard_mouse:", + "category": "objects_symbols", + "aliases": [":keyboard_and_mouse:"], + "aliases_ascii": [], + "keywords": ["computer", "input", "desktop"] + }, + "keyboard_with_jacks": { + "unicode": "1F398", + "unicode_alternates": [], + "name": "musical keyboard with jacks", + "shortname": ":keyboard_with_jacks:", + "category": "objects_symbols", + "aliases": [":musical_keyboard_with_jacks:"], + "aliases_ascii": [], + "keywords": ["music", "instrument", "midi"] + }, + "keycap_ten": { + "unicode": "1F51F", + "unicode_alternates": [], + "name": "keycap ten", + "shortname": ":keycap_ten:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["10", "blue-square", "numbers"], + "moji": "🔟" + }, + "kimono": { + "unicode": "1F458", + "unicode_alternates": [], + "name": "kimono", + "shortname": ":kimono:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["dress", "fashion", "female", "japanese", "women"], + "moji": "👘" + }, + "kiss": { + "unicode": "1F48B", + "unicode_alternates": [], + "name": "kiss mark", + "shortname": ":kiss:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "face", "like", "lips", "love", "valentines"], + "moji": "💋" + }, + "kiss_mm": { + "unicode": "1F468-2764-1F48B-1F468", + "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F48B-200D-1F468"], + "name": "kiss (man,man)", + "shortname": ":kiss_mm:", + "category": "people", + "aliases": [":couplekiss_mm:"], + "aliases_ascii": [], + "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"] + }, + "kiss_ww": { + "unicode": "1F469-2764-1F48B-1F469", + "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F48B-200D-1F469"], + "name": "kiss (woman,woman)", + "shortname": ":kiss_ww:", + "category": "people", + "aliases": [":couplekiss_ww:"], + "aliases_ascii": [], + "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"] + }, + "kissing": { + "unicode": "1F617", + "unicode_alternates": [], + "name": "kissing face", + "shortname": ":kissing:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["3", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "pucker", "lips", "smooch"], + "moji": "😗" + }, + "kissing_cat": { + "unicode": "1F63D", + "unicode_alternates": [], + "name": "kissing cat face with closed eyes", + "shortname": ":kissing_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "passion", "kiss", "puckered", "heart", "love"], + "moji": "😽" + }, + "kissing_closed_eyes": { + "unicode": "1F61A", + "unicode_alternates": [], + "name": "kissing face with closed eyes", + "shortname": ":kissing_closed_eyes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "passion", "puckered", "heart", "love", "smooch"], + "moji": "😚" + }, + "kissing_heart": { + "unicode": "1F618", + "unicode_alternates": [], + "name": "face throwing a kiss", + "shortname": ":kissing_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":*", ":-*", "=*", ":^*"], + "keywords": ["affection", "face", "infatuation", "kiss", "blowing kiss", "heart", "love", "lips", "like", "love", "valentines"], + "moji": "😘" + }, + "kissing_smiling_eyes": { + "unicode": "1F619", + "unicode_alternates": [], + "name": "kissing face with smiling eyes", + "shortname": ":kissing_smiling_eyes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "face", "infatuation", "valentines", "kissing", "kiss", "smile", "pucker", "lips", "smooch"], + "moji": "😙" + }, + "knife": { + "unicode": "1F52A", + "unicode_alternates": [], + "name": "hocho", + "shortname": ":knife:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "🔪" + }, + "koala": { + "unicode": "1F428", + "unicode_alternates": [], + "name": "koala", + "shortname": ":koala:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ¨" + }, + "koko": { + "unicode": "1F201", + "unicode_alternates": [], + "name": "squared katakana koko", + "shortname": ":koko:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "destination", "here", "japanese", "katakana"], + "moji": "ðŸˆ" + }, + "label": { + "unicode": "1F3F7", + "unicode_alternates": [], + "name": "label", + "shortname": ":label:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["tag"] + }, + "large_blue_circle": { + "unicode": "1F535", + "unicode_alternates": [], + "name": "large blue circle", + "shortname": ":large_blue_circle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "🔵" + }, + "large_blue_diamond": { + "unicode": "1F537", + "unicode_alternates": [], + "name": "large blue diamond", + "shortname": ":large_blue_diamond:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔷" + }, + "large_orange_diamond": { + "unicode": "1F536", + "unicode_alternates": [], + "name": "large orange diamond", + "shortname": ":large_orange_diamond:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔶" + }, + "last_quarter_moon": { + "unicode": "1F317", + "unicode_alternates": [], + "name": "last quarter moon symbol", + "shortname": ":last_quarter_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "last", "quarter", "sky", "night", "cheese", "phase"], + "moji": "🌗" + }, + "last_quarter_moon_with_face": { + "unicode": "1F31C", + "unicode_alternates": [], + "name": "last quarter moon with face", + "shortname": ":last_quarter_moon_with_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "last", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "moji": "🌜" + }, + "laughing": { + "unicode": "1F606", + "unicode_alternates": [], + "name": "smiling face with open mouth and tightly-closed ey", + "shortname": ":laughing:", + "category": "emoticons", + "aliases": [":satisfied:"], + "aliases_ascii": [">:)", ">;)", ">:-)", ">=)"], + "keywords": ["happy", "joy", "lol", "smiling", "laughing", "laugh"], + "moji": "😆" + }, + "leaves": { + "unicode": "1F343", + "unicode_alternates": [], + "name": "leaf fluttering in wind", + "shortname": ":leaves:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["grass", "lawn", "nature", "plant", "tree", "vegetable", "leaves", "leaf", "wind", "float", "fluttering"], + "moji": "ðŸƒ" + }, + "ledger": { + "unicode": "1F4D2", + "unicode_alternates": [], + "name": "ledger", + "shortname": ":ledger:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["notes", "paper"], + "moji": "📒" + }, + "left_luggage": { + "unicode": "1F6C5", + "unicode_alternates": [], + "name": "left luggage", + "shortname": ":left_luggage:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "travel", "bag", "baggage", "luggage", "travel"], + "moji": "🛅" + }, + "left_receiver": { + "unicode": "1F57B", + "unicode_alternates": [], + "name": "left hand telephone receiver", + "shortname": ":left_receiver:", + "category": "objects_symbols", + "aliases": [":left_hand_telephone_receiver:"], + "aliases_ascii": [], + "keywords": ["communication", "dial", "technology"] + }, + "left_right_arrow": { + "unicode": "2194", + "unicode_alternates": ["2194-FE0F"], + "name": "left right arrow", + "shortname": ":left_right_arrow:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "↔" + }, + "leftwards_arrow_with_hook": { + "unicode": "21A9", + "unicode_alternates": ["21A9-FE0F"], + "name": "leftwards arrow with hook", + "shortname": ":leftwards_arrow_with_hook:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "↩" + }, + "lemon": { + "unicode": "1F34B", + "unicode_alternates": [], + "name": "lemon", + "shortname": ":lemon:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fruit", "nature", "lemon", "yellow", "citrus"], + "moji": "ðŸ‹" + }, + "leo": { + "unicode": "264C", + "unicode_alternates": ["264C-FE0F"], + "name": "leo", + "shortname": ":leo:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["leo", "lion", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "moji": "♌" + }, + "leopard": { + "unicode": "1F406", + "unicode_alternates": [], + "name": "leopard", + "shortname": ":leopard:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "leopard", "cat", "spot", "spotted", "sexy"], + "moji": "ðŸ†" + }, + "level_slider": { + "unicode": "1F39A", + "unicode_alternates": [], + "name": "level slider", + "shortname": ":level_slider:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["controls"] + }, + "levitate": { + "unicode": "1F574", + "unicode_alternates": [], + "name": "man in business suit levitating", + "shortname": ":levitate:", + "category": "people", + "aliases": [":man_in_business_suit_levitating:"], + "aliases_ascii": [], + "keywords": ["hover", "exclamation"] + }, + "libra": { + "unicode": "264E", + "unicode_alternates": ["264E-FE0F"], + "name": "libra", + "shortname": ":libra:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["libra", "scales", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "moji": "♎" + }, + "lifter": { + "unicode": "1F3CB", + "unicode_alternates": [], + "name": "weight lifter", + "shortname": ":lifter:", + "category": "activity", + "aliases": [":weight_lifter:"], + "aliases_ascii": [], + "keywords": ["bench", "press", "squats", "deadlift"] + }, + "light_check_mark": { + "unicode": "1F5F8", + "unicode_alternates": [], + "name": "light check mark", + "shortname": ":light_check_mark:", + "category": "objects_symbols", + "aliases": [":light_mark:"], + "aliases_ascii": [], + "keywords": ["vote"] + }, + "light_rail": { + "unicode": "1F688", + "unicode_alternates": [], + "name": "light rail", + "shortname": ":light_rail:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "train", "rail", "light"], + "moji": "🚈" + }, + "link": { + "unicode": "1F517", + "unicode_alternates": [], + "name": "link symbol", + "shortname": ":link:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["rings", "url"], + "moji": "🔗" + }, + "lips": { + "unicode": "1F444", + "unicode_alternates": [], + "name": "mouth", + "shortname": ":lips:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["kiss", "mouth"], + "moji": "👄" + }, + "lips2": { + "unicode": "1F5E2", + "unicode_alternates": [], + "name": "lips", + "shortname": ":lips2:", + "category": "people", + "aliases": [], + "aliases_ascii": [], + "keywords": ["kiss", "mouth"] + }, + "lipstick": { + "unicode": "1F484", + "unicode_alternates": [], + "name": "lipstick", + "shortname": ":lipstick:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "female", "girl"], + "moji": "💄" + }, + "lock": { + "unicode": "1F512", + "unicode_alternates": [], + "name": "lock", + "shortname": ":lock:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["password", "security"], + "moji": "🔒" + }, + "lock_with_ink_pen": { + "unicode": "1F50F", + "unicode_alternates": [], + "name": "lock with ink pen", + "shortname": ":lock_with_ink_pen:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["secret", "security"], + "moji": "ðŸ”" + }, + "lollipop": { + "unicode": "1F36D", + "unicode_alternates": [], + "name": "lollipop", + "shortname": ":lollipop:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["candy", "food", "snack", "sweet", "lollipop", "stick", "lick", "sweet", "sugar", "candy"], + "moji": "ðŸ" + }, + "loop": { + "unicode": "27BF", + "unicode_alternates": [], + "name": "double curly loop", + "shortname": ":loop:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["curly"], + "moji": "âž¿" + }, + "loud_sound": { + "unicode": "1F50A", + "unicode_alternates": [], + "name": "speaker with three sound waves", + "shortname": ":loud_sound:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": [], + "moji": "🔊" + }, + "loudspeaker": { + "unicode": "1F4E2", + "unicode_alternates": [], + "name": "public address loudspeaker", + "shortname": ":loudspeaker:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "volume"], + "moji": "📢" + }, + "love_hotel": { + "unicode": "1F3E9", + "unicode_alternates": [], + "name": "love hotel", + "shortname": ":love_hotel:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "dating", "like", "love", "hotel", "love", "sex", "romance", "leisure", "adultery", "prostitution", "hospital", "birth", "happy"], + "moji": "ðŸ©" + }, + "love_letter": { + "unicode": "1F48C", + "unicode_alternates": [], + "name": "love letter", + "shortname": ":love_letter:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "email", "envelope", "like", "valentines", "love", "letter", "kiss", "heart"], + "moji": "💌" + }, + "low_brightness": { + "unicode": "1F505", + "unicode_alternates": [], + "name": "low brightness symbol", + "shortname": ":low_brightness:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["summer", "sun"], + "moji": "🔅" + }, + "m": { + "unicode": "24C2", + "unicode_alternates": ["24C2-FE0F"], + "name": "circled latin capital letter m", + "shortname": ":m:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-circle", "letter"], + "moji": "â“‚" + }, + "mag": { + "unicode": "1F50D", + "unicode_alternates": [], + "name": "left-pointing magnifying glass", + "shortname": ":mag:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"], + "moji": "ðŸ”" + }, + "mag_right": { + "unicode": "1F50E", + "unicode_alternates": [], + "name": "right-pointing magnifying glass", + "shortname": ":mag_right:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"], + "moji": "🔎" + }, + "mahjong": { + "unicode": "1F004", + "unicode_alternates": ["1F004-FE0F"], + "name": "mahjong tile red dragon", + "shortname": ":mahjong:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "game", "kanji"], + "moji": "🀄" + }, + "mailbox": { + "unicode": "1F4EB", + "unicode_alternates": [], + "name": "closed mailbox with raised flag", + "shortname": ":mailbox:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "email", "inbox"], + "moji": "📫" + }, + "mailbox_closed": { + "unicode": "1F4EA", + "unicode_alternates": [], + "name": "closed mailbox with lowered flag", + "shortname": ":mailbox_closed:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "email", "inbox"], + "moji": "📪" + }, + "mailbox_with_mail": { + "unicode": "1F4EC", + "unicode_alternates": [], + "name": "open mailbox with raised flag", + "shortname": ":mailbox_with_mail:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "email", "inbox"], + "moji": "📬" + }, + "mailbox_with_no_mail": { + "unicode": "1F4ED", + "unicode_alternates": [], + "name": "open mailbox with lowered flag", + "shortname": ":mailbox_with_no_mail:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["email", "inbox"], + "moji": "ðŸ“" + }, + "man": { + "unicode": "1F468", + "unicode_alternates": [], + "name": "man", + "shortname": ":man:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["classy", "dad", "father", "guy", "mustashe"], + "moji": "👨" + }, + "man_with_gua_pi_mao": { + "unicode": "1F472", + "unicode_alternates": [], + "name": "man with gua pi mao", + "shortname": ":man_with_gua_pi_mao:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["boy", "male", "skullcap", "chinese", "asian", "qing"], + "moji": "👲" + }, + "man_with_turban": { + "unicode": "1F473", + "unicode_alternates": [], + "name": "man with turban", + "shortname": ":man_with_turban:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["male", "turban", "headdress", "headwear", "pagri", "india", "indian", "mummy", "wisdom", "peace"], + "moji": "👳" + }, + "mans_shoe": { + "unicode": "1F45E", + "unicode_alternates": [], + "name": "mans shoe", + "shortname": ":mans_shoe:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "male"], + "moji": "👞" + }, + "map": { + "unicode": "1F5FA", + "unicode_alternates": [], + "name": "world map", + "shortname": ":map:", + "category": "travel_places", + "aliases": [":world_map:"], + "aliases_ascii": [], + "keywords": ["atlas", "earth", "cartography"] + }, + "maple_leaf": { + "unicode": "1F341", + "unicode_alternates": [], + "name": "maple leaf", + "shortname": ":maple_leaf:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["canada", "nature", "plant", "vegetable", "maple", "leaf", "syrup", "canada", "tree"], + "moji": "ðŸ" + }, + "mask": { + "unicode": "1F637", + "unicode_alternates": [], + "name": "face with medical mask", + "shortname": ":mask:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "ill", "sick", "sick", "virus", "flu", "medical", "mask"], + "moji": "😷" + }, + "massage": { + "unicode": "1F486", + "unicode_alternates": [], + "name": "face massage", + "shortname": ":massage:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman"], + "moji": "💆" + }, + "meat_on_bone": { + "unicode": "1F356", + "unicode_alternates": [], + "name": "meat on bone", + "shortname": ":meat_on_bone:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "good", "meat", "bone", "animal", "cooked"], + "moji": "ðŸ–" + }, + "medal": { + "unicode": "1F3C5", + "unicode_alternates": [], + "name": "sports medal", + "shortname": ":medal:", + "category": "activity", + "aliases": [":sports_medal:"], + "aliases_ascii": [], + "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "first", "show", "reward", "achievement"] + }, + "mega": { + "unicode": "1F4E3", + "unicode_alternates": [], + "name": "cheering megaphone", + "shortname": ":mega:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "speaker", "volume"], + "moji": "📣" + }, + "melon": { + "unicode": "1F348", + "unicode_alternates": [], + "name": "melon", + "shortname": ":melon:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "melon", "cantaloupe", "honeydew"], + "moji": "ðŸˆ" + }, + "mens": { + "unicode": "1F6B9", + "unicode_alternates": [], + "name": "mens symbol", + "shortname": ":mens:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["restroom", "toilet", "wc", "men", "bathroom", "restroom", "sign", "boy", "male", "avatar"], + "moji": "🚹" + }, + "metro": { + "unicode": "1F687", + "unicode_alternates": [], + "name": "metro", + "shortname": ":metro:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "mrt", "transportation", "tube", "underground", "metro", "subway", "underground", "train"], + "moji": "🚇" + }, + "microphone": { + "unicode": "1F3A4", + "unicode_alternates": [], + "name": "microphone", + "shortname": ":microphone:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["PA", "music", "sound", "microphone", "mic", "audio", "sound", "voice", "karaoke"], + "moji": "🎤" + }, + "microphone2": { + "unicode": "1F399", + "unicode_alternates": [], + "name": "studio microphone", + "shortname": ":microphone2:", + "category": "objects_symbols", + "aliases": [":studio_microphone:"], + "aliases_ascii": [], + "keywords": ["mic", "audio", "recording"] + }, + "microscope": { + "unicode": "1F52C", + "unicode_alternates": [], + "name": "microscope", + "shortname": ":microscope:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["experiment", "laboratory", "zoomin"], + "moji": "🔬" + }, + "middle_finger": { + "unicode": "1F595", + "unicode_alternates": [], + "name": "reversed hand with middle finger extended", + "shortname": ":middle_finger:", + "category": "people", + "aliases": [":reversed_hand_with_middle_finger_extended:"], + "aliases_ascii": [], + "keywords": ["fu"] + }, + "military_medal": { + "unicode": "1F396", + "unicode_alternates": [], + "name": "military medal", + "shortname": ":military_medal:", + "category": "celebration", + "aliases": [], + "aliases_ascii": [], + "keywords": ["honor", "acknowledgment", "purple heart", "heroism", "veteran"] + }, + "milky_way": { + "unicode": "1F30C", + "unicode_alternates": [], + "name": "milky way", + "shortname": ":milky_way:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "space", "milky", "galaxy", "star", "stars", "planets", "space", "sky"], + "moji": "🌌" + }, + "minibus": { + "unicode": "1F690", + "unicode_alternates": [], + "name": "minibus", + "shortname": ":minibus:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["car", "transportation", "vehicle", "bus", "city", "transport", "transportation"], + "moji": "ðŸš" + }, + "minidisc": { + "unicode": "1F4BD", + "unicode_alternates": [], + "name": "minidisc", + "shortname": ":minidisc:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["data", "disc", "disk", "record", "technology"], + "moji": "💽" + }, + "mobile_phone_off": { + "unicode": "1F4F4", + "unicode_alternates": [], + "name": "mobile phone off", + "shortname": ":mobile_phone_off:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mute"], + "moji": "📴" + }, + "money_with_wings": { + "unicode": "1F4B8", + "unicode_alternates": [], + "name": "money with wings", + "shortname": ":money_with_wings:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bills", "dollar", "payment", "money", "wings", "easy", "spend", "work", "lost", "blown", "burned", "gift", "cash", "dollar"], + "moji": "💸" + }, + "moneybag": { + "unicode": "1F4B0", + "unicode_alternates": [], + "name": "money bag", + "shortname": ":moneybag:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["coins", "dollar", "payment"], + "moji": "💰" + }, + "monkey": { + "unicode": "1F412", + "unicode_alternates": [], + "name": "monkey", + "shortname": ":monkey:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "monkey", "primate", "banana", "silly"], + "moji": "ðŸ’" + }, + "monkey_face": { + "unicode": "1F435", + "unicode_alternates": [], + "name": "monkey face", + "shortname": ":monkey_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸµ" + }, + "monorail": { + "unicode": "1F69D", + "unicode_alternates": [], + "name": "monorail", + "shortname": ":monorail:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "train", "mono", "rail", "transport"], + "moji": "ðŸš" + }, + "mood_bubble": { + "unicode": "1F5F0", + "unicode_alternates": [], + "name": "mood bubble", + "shortname": ":mood_bubble:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["balloon", "conversation", "communication", "comic", "feeling"] + }, + "mood_bubble_lightning": { + "unicode": "1F5F1", + "unicode_alternates": [], + "name": "lightning mood bubble", + "shortname": ":mood_bubble_lightning:", + "category": "objects_symbols", + "aliases": [":lightning_mood_bubble:"], + "aliases_ascii": [], + "keywords": ["balloon", "conversation", "communication", "comic", "feeling"] + }, + "mood_lightning": { + "unicode": "1F5F2", + "unicode_alternates": [], + "name": "lightning mood", + "shortname": ":mood_lightning:", + "category": "objects_symbols", + "aliases": [":lightning_mood:"], + "aliases_ascii": [], + "keywords": ["zap", "electric", "current"] + }, + "mortar_board": { + "unicode": "1F393", + "unicode_alternates": [], + "name": "graduation cap", + "shortname": ":mortar_board:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cap", "college", "degree", "graduation", "hat", "school", "university", "graduation", "cap", "mortarboard", "academic", "education", "ceremony", "square", "tassel"], + "moji": "🎓" + }, + "motorboat": { + "unicode": "1F6E5", + "unicode_alternates": [], + "name": "motorboat", + "shortname": ":motorboat:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "boat", "speedboat", "powerboat"] + }, + "motorcycle": { + "unicode": "1F3CD", + "unicode_alternates": [], + "name": "racing motorcycle", + "shortname": ":motorcycle:", + "category": "activity", + "aliases": [":racing_motorcycle:"], + "aliases_ascii": [], + "keywords": ["bike", "speed"] + }, + "motorway": { + "unicode": "1F6E3", + "unicode_alternates": [], + "name": "motorway", + "shortname": ":motorway:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["road", "highway", "freeway", "traffic", "travel"] + }, + "mount_fuji": { + "unicode": "1F5FB", + "unicode_alternates": [], + "name": "mount fuji", + "shortname": ":mount_fuji:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japan", "mountain", "nature", "photo"], + "moji": "🗻" + }, + "mountain_bicyclist": { + "unicode": "1F6B5", + "unicode_alternates": [], + "name": "mountain bicyclist", + "shortname": ":mountain_bicyclist:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["human", "sports", "transportation", "bicyclist", "mountain", "bike", "pedal", "bicycle", "transportation"], + "moji": "🚵" + }, + "mountain_cableway": { + "unicode": "1F6A0", + "unicode_alternates": [], + "name": "mountain cableway", + "shortname": ":mountain_cableway:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "mountain", "cable", "rail", "train", "railway"], + "moji": "🚠" + }, + "mountain_railway": { + "unicode": "1F69E", + "unicode_alternates": [], + "name": "mountain railway", + "shortname": ":mountain_railway:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "mountain", "railway", "rail", "train", "transport"], + "moji": "🚞" + }, + "mountain_snow": { + "unicode": "1F3D4", + "unicode_alternates": [], + "name": "snow capped mountain", + "shortname": ":mountain_snow:", + "category": "travel_places", + "aliases": [":snow_capped_mountain:"], + "aliases_ascii": [], + "keywords": ["cold", "elevation", "hiking", "peak"] + }, + "mouse": { + "unicode": "1F42D", + "unicode_alternates": [], + "name": "mouse face", + "shortname": ":mouse:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ" + }, + "mouse2": { + "unicode": "1F401", + "unicode_alternates": [], + "name": "mouse", + "shortname": ":mouse2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "mouse", "mice", "rodent"], + "moji": "ðŸ" + }, + "mouse_one": { + "unicode": "1F5AF", + "unicode_alternates": [], + "name": "one button mouse", + "shortname": ":mouse_one:", + "category": "objects_symbols", + "aliases": [":one_button_mouse:"], + "aliases_ascii": [], + "keywords": ["computer", "input", "device"] + }, + "movie_camera": { + "unicode": "1F3A5", + "unicode_alternates": [], + "name": "movie camera", + "shortname": ":movie_camera:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["film", "record", "movie", "camera", "camcorder", "video", "motion", "picture"], + "moji": "🎥" + }, + "moyai": { + "unicode": "1F5FF", + "unicode_alternates": [], + "name": "moyai", + "shortname": ":moyai:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["island", "stone"], + "moji": "🗿" + }, + "muscle": { + "unicode": "1F4AA", + "unicode_alternates": [], + "name": "flexed biceps", + "shortname": ":muscle:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arm", "flex", "hand", "strong", "muscle", "bicep"], + "moji": "💪" + }, + "mushroom": { + "unicode": "1F344", + "unicode_alternates": [], + "name": "mushroom", + "shortname": ":mushroom:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["plant", "vegetable", "mushroom", "fungi", "food", "fungus"], + "moji": "ðŸ„" + }, + "musical_keyboard": { + "unicode": "1F3B9", + "unicode_alternates": [], + "name": "musical keyboard", + "shortname": ":musical_keyboard:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["instrument", "piano", "music", "keyboard", "piano", "organ", "instrument", "electric"], + "moji": "🎹" + }, + "musical_note": { + "unicode": "1F3B5", + "unicode_alternates": [], + "name": "musical note", + "shortname": ":musical_note:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["score", "musical", "music", "note", "music", "sound"], + "moji": "🎵" + }, + "musical_score": { + "unicode": "1F3BC", + "unicode_alternates": [], + "name": "musical score", + "shortname": ":musical_score:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["clef", "treble", "music", "musical", "score", "clef", "g-clef", "stave", "staff"], + "moji": "🎼" + }, + "mute": { + "unicode": "1F507", + "unicode_alternates": [], + "name": "speaker with cancellation stroke", + "shortname": ":mute:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "volume"], + "moji": "🔇" + }, + "nail_care": { + "unicode": "1F485", + "unicode_alternates": [], + "name": "nail polish", + "shortname": ":nail_care:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beauty", "manicure"], + "moji": "💅" + }, + "name_badge": { + "unicode": "1F4DB", + "unicode_alternates": [], + "name": "name badge", + "shortname": ":name_badge:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fire", "forbid"], + "moji": "📛" + }, + "necktie": { + "unicode": "1F454", + "unicode_alternates": [], + "name": "necktie", + "shortname": ":necktie:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cloth", "fashion", "formal", "shirt", "suitup"], + "moji": "👔" + }, + "negative_squared_cross_mark": { + "unicode": "274E", + "unicode_alternates": [], + "name": "negative squared cross mark", + "shortname": ":negative_squared_cross_mark:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["deny", "green-square", "no", "x"], + "moji": "âŽ" + }, + "network": { + "unicode": "1F5A7", + "unicode_alternates": [], + "name": "three networked computers", + "shortname": ":network:", + "category": "objects_symbols", + "aliases": [":three_networked_computers:"], + "aliases_ascii": [], + "keywords": ["lan", "wan", "network", "technology"] + }, + "neutral_face": { + "unicode": "1F610", + "unicode_alternates": [], + "name": "neutral face", + "shortname": ":neutral_face:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "indifference", "neutral", "objective", "impartial", "blank"], + "moji": "ðŸ˜" + }, + "new": { + "unicode": "1F195", + "unicode_alternates": [], + "name": "squared new", + "shortname": ":new:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "🆕" + }, + "new_moon": { + "unicode": "1F311", + "unicode_alternates": [], + "name": "new moon symbol", + "shortname": ":new_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "new", "sky", "night", "cheese", "phase"], + "moji": "🌑" + }, + "new_moon_with_face": { + "unicode": "1F31A", + "unicode_alternates": [], + "name": "new moon with face", + "shortname": ":new_moon_with_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "new", "anthropomorphic", "face", "sky", "night", "cheese", "phase"], + "moji": "🌚" + }, + "newspaper": { + "unicode": "1F4F0", + "unicode_alternates": [], + "name": "newspaper", + "shortname": ":newspaper:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["headline", "press"], + "moji": "📰" + }, + "newspaper2": { + "unicode": "1F5DE", + "unicode_alternates": [], + "name": "rolled-up newspaper", + "shortname": ":newspaper2:", + "category": "objects_symbols", + "aliases": [":rolled_up_newspaper:"], + "aliases_ascii": [], + "keywords": ["headline", "press"] + }, + "night_with_stars": { + "unicode": "1F303", + "unicode_alternates": [], + "name": "night with stars", + "shortname": ":night_with_stars:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "star", "cloudless", "evening", "planets", "space", "sky"], + "moji": "🌃" + }, + "nine": { + "moji": "9ï¸âƒ£", + "unicode": "0039-20E3", + "unicode_alternates": ["0039-FE0F-20E3"], + "name": "digit nine", + "shortname": ":nine:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["9", "blue-square", "numbers"] + }, + "no_bell": { + "unicode": "1F515", + "unicode_alternates": [], + "name": "bell with cancellation stroke", + "shortname": ":no_bell:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mute", "sound", "volume"], + "moji": "🔕" + }, + "no_bicycles": { + "unicode": "1F6B3", + "unicode_alternates": [], + "name": "no bicycles", + "shortname": ":no_bicycles:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cyclist", "prohibited", "bicycle", "bike pedal", "no"], + "moji": "🚳" + }, + "no_entry": { + "unicode": "26D4", + "unicode_alternates": ["26D4-FE0F"], + "name": "no entry", + "shortname": ":no_entry:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bad", "denied", "limit", "privacy", "security", "stop"], + "moji": "â›”" + }, + "no_entry_sign": { + "unicode": "1F6AB", + "unicode_alternates": [], + "name": "no entry sign", + "shortname": ":no_entry_sign:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["denied", "disallow", "forbid", "limit", "stop", "no", "stop", "entry"], + "moji": "🚫" + }, + "no_good": { + "unicode": "1F645", + "unicode_alternates": [], + "name": "face with no good gesture", + "shortname": ":no_good:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman", "no", "stop", "nope", "don't", "not"], + "moji": "🙅" + }, + "no_mobile_phones": { + "unicode": "1F4F5", + "unicode_alternates": [], + "name": "no mobile phones", + "shortname": ":no_mobile_phones:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["iphone", "mute"], + "moji": "📵" + }, + "no_mouth": { + "unicode": "1F636", + "unicode_alternates": [], + "name": "face without mouth", + "shortname": ":no_mouth:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":-X", ":X", ":-#", ":#", "=X", "=x", ":x", ":-x", "=#"], + "keywords": ["face", "hellokitty", "mouth", "silent", "vapid"], + "moji": "😶" + }, + "no_pedestrians": { + "unicode": "1F6B7", + "unicode_alternates": [], + "name": "no pedestrians", + "shortname": ":no_pedestrians:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["crossing", "rules", "walking", "no", "walk", "pedestrian", "stroll", "stride", "foot", "feet"], + "moji": "🚷" + }, + "no_smoking": { + "unicode": "1F6AD", + "unicode_alternates": [], + "name": "no smoking symbol", + "shortname": ":no_smoking:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cigarette", "no", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"], + "moji": "ðŸš" + }, + "non-potable_water": { + "unicode": "1F6B1", + "unicode_alternates": [], + "name": "non-potable water symbol", + "shortname": ":non-potable_water:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["drink", "faucet", "tap", "non-potable", "water", "not drinkable", "dirty", "gross", "aqua", "h20"], + "moji": "🚱" + }, + "nose": { + "unicode": "1F443", + "unicode_alternates": [], + "name": "nose", + "shortname": ":nose:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["smell", "sniff"], + "moji": "👃" + }, + "note": { + "unicode": "1F5C9", + "unicode_alternates": [], + "name": "note page", + "shortname": ":note:", + "category": "objects_symbols", + "aliases": [":note_page:"], + "aliases_ascii": [], + "keywords": ["stationery", "post-it"] + }, + "note_empty": { + "unicode": "1F5C6", + "unicode_alternates": [], + "name": "empty note page", + "shortname": ":note_empty:", + "category": "objects_symbols", + "aliases": [":empty_note_page:"], + "aliases_ascii": [], + "keywords": ["stationery", "post-it"] + }, + "notebook": { + "unicode": "1F4D3", + "unicode_alternates": [], + "name": "notebook", + "shortname": ":notebook:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["notes", "paper", "record", "stationery"], + "moji": "📓" + }, + "notebook_with_decorative_cover": { + "unicode": "1F4D4", + "unicode_alternates": [], + "name": "notebook with decorative cover", + "shortname": ":notebook_with_decorative_cover:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["classroom", "notes", "paper", "record"], + "moji": "📔" + }, + "notepad": { + "unicode": "1F5CA", + "unicode_alternates": [], + "name": "note pad", + "shortname": ":notepad:", + "category": "objects_symbols", + "aliases": [":note_pad:"], + "aliases_ascii": [], + "keywords": ["stationery", "post-it"] + }, + "notepad_empty": { + "unicode": "1F5C7", + "unicode_alternates": [], + "name": "empty note pad", + "shortname": ":notepad_empty:", + "category": "objects_symbols", + "aliases": [":empty_note_pad:"], + "aliases_ascii": [], + "keywords": ["stationery", "post-it"] + }, + "notepad_spiral": { + "unicode": "1F5D2", + "unicode_alternates": [], + "name": "spiral note pad", + "shortname": ":notepad_spiral:", + "category": "objects_symbols", + "aliases": [":spiral_note_pad:"], + "aliases_ascii": [], + "keywords": ["stationery"] + }, + "notes": { + "unicode": "1F3B6", + "unicode_alternates": [], + "name": "multiple musical notes", + "shortname": ":notes:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["music", "score", "musical", "music", "notes", "music", "sound", "melody"], + "moji": "🎶" + }, + "nut_and_bolt": { + "unicode": "1F529", + "unicode_alternates": [], + "name": "nut and bolt", + "shortname": ":nut_and_bolt:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["handy", "tools"], + "moji": "🔩" + }, + "o": { + "unicode": "2B55", + "unicode_alternates": ["2B55-FE0F"], + "name": "heavy large circle", + "shortname": ":o:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["circle", "round"], + "moji": "â•" + }, + "o2": { + "unicode": "1F17E", + "unicode_alternates": [], + "name": "negative squared latin capital letter o", + "shortname": ":o2:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "letter", "red-square"], + "moji": "🅾" + }, + "ocean": { + "unicode": "1F30A", + "unicode_alternates": [], + "name": "water wave", + "shortname": ":ocean:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sea", "water", "wave", "ocean", "wave", "surf", "beach", "tide"], + "moji": "🌊" + }, + "octopus": { + "unicode": "1F419", + "unicode_alternates": [], + "name": "octopus", + "shortname": ":octopus:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "creature", "ocean", "sea"], + "moji": "ðŸ™" + }, + "oden": { + "unicode": "1F362", + "unicode_alternates": [], + "name": "oden", + "shortname": ":oden:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "japanese", "oden", "seafood", "casserole", "stew"], + "moji": "ðŸ¢" + }, + "office": { + "unicode": "1F3E2", + "unicode_alternates": [], + "name": "office building", + "shortname": ":office:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "bureau", "work"], + "moji": "ðŸ¢" + }, + "oil": { + "unicode": "1F6E2", + "unicode_alternates": [], + "name": "oil drum", + "shortname": ":oil:", + "category": "objects_symbols", + "aliases": [":oil_drum:"], + "aliases_ascii": [], + "keywords": ["petroleum"] + }, + "ok": { + "unicode": "1F197", + "unicode_alternates": [], + "name": "squared ok", + "shortname": ":ok:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["agree", "blue-square", "good", "yes"], + "moji": "🆗" + }, + "ok_hand": { + "unicode": "1F44C", + "unicode_alternates": [], + "name": "ok hand sign", + "shortname": ":ok_hand:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fingers", "limbs", "perfect", "okay", "ok", "smoke", "smoking", "marijuana", "joint", "pot", "420"], + "moji": "👌" + }, + "ok_woman": { + "unicode": "1F646", + "unicode_alternates": [], + "name": "face with ok gesture", + "shortname": ":ok_woman:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["*\\0/*", "\\0/", "*\\O/*", "\\O/"], + "keywords": ["female", "girl", "human", "pink", "women", "yes", "ok", "okay", "accept"], + "moji": "🙆" + }, + "older_man": { + "unicode": "1F474", + "unicode_alternates": [], + "name": "older man", + "shortname": ":older_man:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["human", "male", "men"], + "moji": "👴" + }, + "older_woman": { + "unicode": "1F475", + "unicode_alternates": [], + "name": "older woman", + "shortname": ":older_woman:", + "category": "emoticons", + "aliases": [":grandma:"], + "aliases_ascii": [], + "keywords": ["female", "girl", "women", "grandma", "grandmother"], + "moji": "👵" + }, + "om_symbol": { + "unicode": "1F549", + "unicode_alternates": [], + "name": "om symbol", + "shortname": ":om_symbol:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hinduism", "sound", "spiritual", "icon", "dharmic", "buddhism", "jainism", "meditate"] + }, + "on": { + "unicode": "1F51B", + "unicode_alternates": [], + "name": "on with exclamation mark with left right arrow abo", + "shortname": ":on:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "words"], + "moji": "🔛" + }, + "oncoming_automobile": { + "unicode": "1F698", + "unicode_alternates": [], + "name": "oncoming automobile", + "shortname": ":oncoming_automobile:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["car", "transportation", "vehicle", "sedan", "car", "automobile"], + "moji": "🚘" + }, + "oncoming_bus": { + "unicode": "1F68D", + "unicode_alternates": [], + "name": "oncoming bus", + "shortname": ":oncoming_bus:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "bus", "school", "city", "transportation", "public"], + "moji": "ðŸš" + }, + "oncoming_police_car": { + "unicode": "1F694", + "unicode_alternates": [], + "name": "oncoming police car", + "shortname": ":oncoming_police_car:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["enforcement", "law", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"], + "moji": "🚔" + }, + "oncoming_taxi": { + "unicode": "1F696", + "unicode_alternates": [], + "name": "oncoming taxi", + "shortname": ":oncoming_taxi:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"], + "moji": "🚖" + }, + "one": { + "moji": "1ï¸âƒ£", + "unicode": "0031-20E3", + "unicode_alternates": ["0031-FE0F-20E3"], + "name": "digit one", + "shortname": ":one:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["1", "blue-square", "numbers"] + }, + "open_file_folder": { + "unicode": "1F4C2", + "unicode_alternates": [], + "name": "open file folder", + "shortname": ":open_file_folder:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents", "load"], + "moji": "📂" + }, + "open_hands": { + "unicode": "1F450", + "unicode_alternates": [], + "name": "open hands sign", + "shortname": ":open_hands:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["butterfly", "fingers"], + "moji": "ðŸ‘" + }, + "open_mouth": { + "unicode": "1F62E", + "unicode_alternates": [], + "name": "face with open mouth", + "shortname": ":open_mouth:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":-O", ":O", ":-o", ":o", "O_O", ">:O"], + "keywords": ["face", "impressed", "mouth", "open", "jaw", "gapping", "surprise", "wow"], + "moji": "😮" + }, + "ophiuchus": { + "unicode": "26CE", + "unicode_alternates": [], + "name": "ophiuchus", + "shortname": ":ophiuchus:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ophiuchus", "serpent", "snake", "astrology", "greek", "constellation", "stars", "zodiac", "purple-square", "sign", "horoscope"], + "moji": "⛎" + }, + "optical_disk": { + "unicode": "1F5B8", + "unicode_alternates": [], + "name": "optical disc icon", + "shortname": ":optical_disk:", + "category": "objects_symbols", + "aliases": [":optical_disc_icon:"], + "aliases_ascii": [], + "keywords": ["cd", "dvd", "disc", "disk", "technology"] + }, + "orange_book": { + "unicode": "1F4D9", + "unicode_alternates": [], + "name": "orange book", + "shortname": ":orange_book:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["knowledge", "library", "read"], + "moji": "📙" + }, + "outbox_tray": { + "unicode": "1F4E4", + "unicode_alternates": [], + "name": "outbox tray", + "shortname": ":outbox_tray:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["email", "inbox"], + "moji": "📤" + }, + "ox": { + "unicode": "1F402", + "unicode_alternates": [], + "name": "ox", + "shortname": ":ox:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "beef", "cow"], + "moji": "ðŸ‚" + }, + "package": { + "unicode": "1F4E6", + "unicode_alternates": [], + "name": "package", + "shortname": ":package:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gift", "mail"], + "moji": "📦" + }, + "page": { + "unicode": "1F5CF", + "unicode_alternates": [], + "name": "page", + "shortname": ":page:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["document"] + }, + "page_facing_up": { + "unicode": "1F4C4", + "unicode_alternates": [], + "name": "page facing up", + "shortname": ":page_facing_up:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"], + "moji": "📄" + }, + "page_with_curl": { + "unicode": "1F4C3", + "unicode_alternates": [], + "name": "page with curl", + "shortname": ":page_with_curl:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"], + "moji": "📃" + }, + "pager": { + "unicode": "1F4DF", + "unicode_alternates": [], + "name": "pager", + "shortname": ":pager:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bbcall", "oldschool"], + "moji": "📟" + }, + "pages": { + "unicode": "1F5D0", + "unicode_alternates": [], + "name": "pages", + "shortname": ":pages:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"] + }, + "paintbrush": { + "unicode": "1F58C", + "unicode_alternates": [], + "name": "lower left paintbrush", + "shortname": ":paintbrush:", + "category": "objects_symbols", + "aliases": [":lower_left_paintbrush:"], + "aliases_ascii": [], + "keywords": ["brush", "art", "painting"] + }, + "palm_tree": { + "unicode": "1F334", + "unicode_alternates": [], + "name": "palm tree", + "shortname": ":palm_tree:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "vegetable", "palm", "tree", "coconuts", "fronds", "warm", "tropical"], + "moji": "🌴" + }, + "panda_face": { + "unicode": "1F43C", + "unicode_alternates": [], + "name": "panda face", + "shortname": ":panda_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "panda", "bear", "face", "cub", "cute", "endearment", "friendship", "love", "bamboo", "china", "black", "white"], + "moji": "ðŸ¼" + }, + "paperclip": { + "unicode": "1F4CE", + "unicode_alternates": [], + "name": "paperclip", + "shortname": ":paperclip:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents", "stationery"], + "moji": "📎" + }, + "paperclips": { + "unicode": "1F587", + "unicode_alternates": [], + "name": "linked paperclips", + "shortname": ":paperclips:", + "category": "objects_symbols", + "aliases": [":linked_paperclips:"], + "aliases_ascii": [], + "keywords": ["documents", "stationery"] + }, + "park": { + "unicode": "1F3DE", + "unicode_alternates": [], + "name": "national park", + "shortname": ":park:", + "category": "travel_places", + "aliases": [":national_park:"], + "aliases_ascii": [], + "keywords": ["woods", "nature", "wildlife", "forest", "wilderness", "national"] + }, + "parking": { + "unicode": "1F17F", + "unicode_alternates": ["1F17F-FE0F"], + "name": "negative squared latin capital letter p", + "shortname": ":parking:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "blue-square", "cars", "letter"], + "moji": "🅿" + }, + "part_alternation_mark": { + "unicode": "303D", + "unicode_alternates": ["303D-FE0F"], + "name": "part alternation mark", + "shortname": ":part_alternation_mark:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph", "sing", "song", "vocal", "music", "karaoke", "cue", "letter", "m", "japanese"], + "moji": "〽" + }, + "partly_sunny": { + "unicode": "26C5", + "unicode_alternates": ["26C5-FE0F"], + "name": "sun behind cloud", + "shortname": ":partly_sunny:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cloud", "morning", "nature", "weather"], + "moji": "â›…" + }, + "passport_control": { + "unicode": "1F6C2", + "unicode_alternates": [], + "name": "passport control", + "shortname": ":passport_control:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "custom", "passport", "official", "travel", "control", "foreign", "identification"], + "moji": "🛂" + }, + "peach": { + "unicode": "1F351", + "unicode_alternates": [], + "name": "peach", + "shortname": ":peach:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "peach", "fruit", "juicy", "pit"], + "moji": "ðŸ‘" + }, + "pear": { + "unicode": "1F350", + "unicode_alternates": [], + "name": "pear", + "shortname": ":pear:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fruit", "nature", "pear", "fruit", "shape"], + "moji": "ðŸ" + }, + "pen_ballpoint": { + "unicode": "1F58A", + "unicode_alternates": [], + "name": "lower left ballpoint pen", + "shortname": ":pen_ballpoint:", + "category": "objects_symbols", + "aliases": [":lower_left_ballpoint_pen:"], + "aliases_ascii": [], + "keywords": ["write", "bic", "ink"] + }, + "pen_fountain": { + "unicode": "1F58B", + "unicode_alternates": [], + "name": "lower left fountain pen", + "shortname": ":pen_fountain:", + "category": "objects_symbols", + "aliases": [":lower_left_fountain_pen:"], + "aliases_ascii": [], + "keywords": ["write", "calligraphy", "ink"] + }, + "pencil": { + "unicode": "1F4DD", + "unicode_alternates": [], + "name": "memo", + "shortname": ":pencil:", + "category": "objects", + "aliases": [":memo:"], + "aliases_ascii": [], + "keywords": ["documents", "paper", "station", "write"], + "moji": "ðŸ“" + }, + "pencil2": { + "unicode": "270F", + "unicode_alternates": ["270F-FE0F"], + "name": "pencil", + "shortname": ":pencil2:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["paper", "stationery", "write"], + "moji": "âœ" + }, + "pencil3": { + "unicode": "1F589", + "unicode_alternates": [], + "name": "lower left pencil", + "shortname": ":pencil3:", + "category": "objects_symbols", + "aliases": [":lower_left_pencil:"], + "aliases_ascii": [], + "keywords": ["paper", "stationery", "write"] + }, + "penguin": { + "unicode": "1F427", + "unicode_alternates": [], + "name": "penguin", + "shortname": ":penguin:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ§" + }, + "pennant_black": { + "unicode": "1F3F2", + "unicode_alternates": [], + "name": "black pennant", + "shortname": ":pennant_black:", + "category": "objects_symbols", + "aliases": [":black_pennant:"], + "aliases_ascii": [], + "keywords": ["flag", "athletics"] + }, + "pennant_white": { + "unicode": "1F3F1", + "unicode_alternates": [], + "name": "white pennant", + "shortname": ":pennant_white:", + "category": "objects_symbols", + "aliases": [":white_pennant:"], + "aliases_ascii": [], + "keywords": ["flag", "athletics"] + }, + "pensive": { + "unicode": "1F614", + "unicode_alternates": [], + "name": "pensive face", + "shortname": ":pensive:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "okay", "sad", "pensive", "thoughtful", "think", "reflective", "wistful", "meditate", "serious"], + "moji": "😔" + }, + "performing_arts": { + "unicode": "1F3AD", + "unicode_alternates": [], + "name": "performing arts", + "shortname": ":performing_arts:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["acting", "drama", "theater", "performing", "arts", "performance", "entertainment", "acting", "story", "mask", "masks"], + "moji": "ðŸŽ" + }, + "persevere": { + "unicode": "1F623", + "unicode_alternates": [], + "name": "persevering face", + "shortname": ":persevere:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [">.<"], + "keywords": ["endure", "persevere", "face", "no", "sick", "upset"], + "moji": "😣" + }, + "person_frowning": { + "unicode": "1F64D", + "unicode_alternates": [], + "name": "person frowning", + "shortname": ":person_frowning:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman", "dejected", "rejected", "sad", "frown"], + "moji": "ðŸ™" + }, + "person_with_blond_hair": { + "unicode": "1F471", + "unicode_alternates": [], + "name": "person with blond hair", + "shortname": ":person_with_blond_hair:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["male", "man", "blonde", "young", "western", "westerner", "occidental"], + "moji": "👱" + }, + "person_with_pouting_face": { + "unicode": "1F64E", + "unicode_alternates": [], + "name": "person with pouting face", + "shortname": ":person_with_pouting_face:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman", "pout", "sexy", "cute", "annoyed"], + "moji": "🙎" + }, + "pig": { + "unicode": "1F437", + "unicode_alternates": [], + "name": "pig face", + "shortname": ":pig:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "oink"], + "moji": "ðŸ·" + }, + "pig2": { + "unicode": "1F416", + "unicode_alternates": [], + "name": "pig", + "shortname": ":pig2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "pig", "piggy", "pork", "ham", "hog", "bacon", "oink", "slop", "livestock", "greed", "greedy"], + "moji": "ðŸ–" + }, + "pig_nose": { + "unicode": "1F43D", + "unicode_alternates": [], + "name": "pig nose", + "shortname": ":pig_nose:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "oink", "pig", "nose", "snout", "food", "eat", "cute", "oink", "pink", "smell", "truffle"], + "moji": "ðŸ½" + }, + "pill": { + "unicode": "1F48A", + "unicode_alternates": [], + "name": "pill", + "shortname": ":pill:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["health", "medicine"], + "moji": "💊" + }, + "pineapple": { + "unicode": "1F34D", + "unicode_alternates": [], + "name": "pineapple", + "shortname": ":pineapple:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "pineapple", "pina", "tropical", "flower"], + "moji": "ðŸ" + }, + "piracy": { + "unicode": "1F572", + "unicode_alternates": [], + "name": "no piracy", + "shortname": ":piracy:", + "category": "objects_symbols", + "aliases": [":no_piracy:"], + "aliases_ascii": [], + "keywords": ["theft", "rule"] + }, + "pisces": { + "unicode": "2653", + "unicode_alternates": ["2653-FE0F"], + "name": "pisces", + "shortname": ":pisces:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["pisces", "fish", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"], + "moji": "♓" + }, + "pizza": { + "unicode": "1F355", + "unicode_alternates": [], + "name": "slice of pizza", + "shortname": ":pizza:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "party", "pizza", "pie", "new york", "italian", "italy", "slice", "peperoni"], + "moji": "ðŸ•" + }, + "point_down": { + "unicode": "1F447", + "unicode_alternates": [], + "name": "white down pointing backhand index", + "shortname": ":point_down:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["direction", "fingers", "hand"], + "moji": "👇" + }, + "point_left": { + "unicode": "1F448", + "unicode_alternates": [], + "name": "white left pointing backhand index", + "shortname": ":point_left:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["direction", "fingers", "hand"], + "moji": "👈" + }, + "point_right": { + "unicode": "1F449", + "unicode_alternates": [], + "name": "white right pointing backhand index", + "shortname": ":point_right:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["direction", "fingers", "hand"], + "moji": "👉" + }, + "point_up": { + "unicode": "261D", + "unicode_alternates": ["261D-FE0F"], + "name": "white up pointing index", + "shortname": ":point_up:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["direction", "fingers", "hand"], + "moji": "â˜" + }, + "point_up_2": { + "unicode": "1F446", + "unicode_alternates": [], + "name": "white up pointing backhand index", + "shortname": ":point_up_2:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["direction", "fingers", "hand"], + "moji": "👆" + }, + "police_car": { + "unicode": "1F693", + "unicode_alternates": [], + "name": "police car", + "shortname": ":police_car:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "enforcement", "law", "transportation", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"], + "moji": "🚓" + }, + "poodle": { + "unicode": "1F429", + "unicode_alternates": [], + "name": "poodle", + "shortname": ":poodle:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["101", "animal", "dog", "nature", "poodle", "dog", "clip", "showy", "sophisticated", "vain"], + "moji": "ðŸ©" + }, + "poop": { + "unicode": "1F4A9", + "unicode_alternates": [], + "name": "pile of poo", + "shortname": ":poop:", + "category": "emoticons", + "aliases": [":shit:", ":hankey:", ":poo:"], + "aliases_ascii": [], + "keywords": ["poop", "shit", "shitface", "turd", "poo"], + "moji": "💩" + }, + "post_office": { + "unicode": "1F3E3", + "unicode_alternates": [], + "name": "japanese post office", + "shortname": ":post_office:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "communication", "email"], + "moji": "ðŸ£" + }, + "postal_horn": { + "unicode": "1F4EF", + "unicode_alternates": [], + "name": "postal horn", + "shortname": ":postal_horn:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["instrument", "music"], + "moji": "📯" + }, + "postbox": { + "unicode": "1F4EE", + "unicode_alternates": [], + "name": "postbox", + "shortname": ":postbox:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["email", "envelope", "letter"], + "moji": "📮" + }, + "potable_water": { + "unicode": "1F6B0", + "unicode_alternates": [], + "name": "potable water symbol", + "shortname": ":potable_water:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "cleaning", "faucet", "liquid", "restroom", "potable", "water", "drinkable", "pure", "clear", "clean", "aqua", "h20"], + "moji": "🚰" + }, + "pouch": { + "unicode": "1F45D", + "unicode_alternates": [], + "name": "pouch", + "shortname": ":pouch:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "bag", "pouch", "bag", "cosmetic", "packing", "grandma", "makeup"], + "moji": "ðŸ‘" + }, + "poultry_leg": { + "unicode": "1F357", + "unicode_alternates": [], + "name": "poultry leg", + "shortname": ":poultry_leg:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "meat", "poultry", "leg", "chicken", "fried"], + "moji": "ðŸ—" + }, + "pound": { + "unicode": "1F4B7", + "unicode_alternates": [], + "name": "banknote with pound sign", + "shortname": ":pound:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bills", "british", "currency", "england", "money", "sterling", "uk", "pound", "britain", "british", "banknote", "money", "currency", "paper", "cash", "bills"], + "moji": "💷" + }, + "pouting_cat": { + "unicode": "1F63E", + "unicode_alternates": [], + "name": "pouting cat face", + "shortname": ":pouting_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "pout", "annoyed", "miffed", "glower", "frown"], + "moji": "😾" + }, + "pray": { + "unicode": "1F64F", + "unicode_alternates": [], + "name": "person with folded hands", + "shortname": ":pray:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["highfive", "hope", "namaste", "please", "wish", "pray", "high five", "hands", "sorrow", "regret", "sorry"], + "moji": "ðŸ™" + }, + "princess": { + "unicode": "1F478", + "unicode_alternates": [], + "name": "princess", + "shortname": ":princess:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blond", "crown", "female", "girl", "woman", "princess", "royal", "royalty", "king", "queen", "daughter", "disney", "high-maintenance"], + "moji": "👸" + }, + "printer": { + "unicode": "1F5A8", + "unicode_alternates": [], + "name": "printer", + "shortname": ":printer:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hardcopy", "paper", "inkjet", "laser"] + }, + "prohibited": { + "unicode": "1F6C7", + "unicode_alternates": [], + "name": "prohibited sign", + "shortname": ":prohibited:", + "category": "objects_symbols", + "aliases": [":prohibited_sign:"], + "aliases_ascii": [], + "keywords": ["no", "not", "denied", "disallow", "forbid", "limit", "stop"] + }, + "projector": { + "unicode": "1F4FD", + "unicode_alternates": [], + "name": "film projector", + "shortname": ":projector:", + "category": "objects_symbols", + "aliases": [":film_projector:"], + "aliases_ascii": [], + "keywords": ["movie", "video", "motion", "picture", "8mm", "16mm"] + }, + "punch": { + "unicode": "1F44A", + "unicode_alternates": [], + "name": "fisted hand sign", + "shortname": ":punch:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fist", "hand"], + "moji": "👊" + }, + "purple_heart": { + "unicode": "1F49C", + "unicode_alternates": [], + "name": "purple heart", + "shortname": ":purple_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "purple", "violet", "heart", "love", "sensitive", "understanding", "compassionate", "compassion", "duty", "honor", "royalty", "veteran", "sacrifice"], + "moji": "💜" + }, + "purse": { + "unicode": "1F45B", + "unicode_alternates": [], + "name": "purse", + "shortname": ":purse:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "fashion", "money", "purse", "clutch", "bag", "handbag", "coin bag", "accessory", "money", "ladies", "shopping"], + "moji": "👛" + }, + "pushpin": { + "unicode": "1F4CC", + "unicode_alternates": [], + "name": "pushpin", + "shortname": ":pushpin:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["stationery"], + "moji": "📌" + }, + "pushpin_black": { + "unicode": "1F588", + "unicode_alternates": [], + "name": "black pushpin", + "shortname": ":pushpin_black:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["stationery"] + }, + "put_litter_in_its_place": { + "unicode": "1F6AE", + "unicode_alternates": [], + "name": "put litter in its place symbol", + "shortname": ":put_litter_in_its_place:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "litter", "waste", "trash", "garbage", "receptacle", "can"], + "moji": "🚮" + }, + "question": { + "unicode": "2753", + "unicode_alternates": [], + "name": "black question mark ornament", + "shortname": ":question:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["confused", "doubt"], + "moji": "â“" + }, + "rabbit": { + "unicode": "1F430", + "unicode_alternates": [], + "name": "rabbit face", + "shortname": ":rabbit:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸ°" + }, + "rabbit2": { + "unicode": "1F407", + "unicode_alternates": [], + "name": "rabbit", + "shortname": ":rabbit2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "rabbit", "bunny", "easter", "reproduction", "prolific"], + "moji": "ðŸ‡" + }, + "race_car": { + "unicode": "1F3CE", + "unicode_alternates": [], + "name": "racing car", + "shortname": ":race_car:", + "category": "activity", + "aliases": [":racing_car:"], + "aliases_ascii": [], + "keywords": ["formula 1", "race", "stock", "nascar", "speed", "drive"] + }, + "racehorse": { + "unicode": "1F40E", + "unicode_alternates": [], + "name": "horse", + "shortname": ":racehorse:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "gamble", "horse", "powerful", "draft", "calvary", "cowboy", "cowgirl", "mounted", "race", "ride", "gallop", "trot", "colt", "filly", "mare", "stallion", "gelding", "yearling", "thoroughbred", "pony"], + "moji": "ðŸŽ" + }, + "radio": { + "unicode": "1F4FB", + "unicode_alternates": [], + "name": "radio", + "shortname": ":radio:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "music", "podcast", "program"], + "moji": "📻" + }, + "radio_button": { + "unicode": "1F518", + "unicode_alternates": [], + "name": "radio button", + "shortname": ":radio_button:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["input"], + "moji": "🔘" + }, + "rage": { + "unicode": "1F621", + "unicode_alternates": [], + "name": "pouting face", + "shortname": ":rage:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["angry", "despise", "hate", "mad", "pout", "anger", "rage", "irate"], + "moji": "😡" + }, + "railway_car": { + "unicode": "1F683", + "unicode_alternates": [], + "name": "railway car", + "shortname": ":railway_car:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "railway", "rail", "car", "coach", "train"], + "moji": "🚃" + }, + "railway_track": { + "unicode": "1F6E4", + "unicode_alternates": [], + "name": "railway track", + "shortname": ":railway_track:", + "category": "travel_places", + "aliases": [":railroad_track:"], + "aliases_ascii": [], + "keywords": ["train", "trolley", "subway", "locomotive", "transit"] + }, + "rainbow": { + "unicode": "1F308", + "unicode_alternates": [], + "name": "rainbow", + "shortname": ":rainbow:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["happy", "nature", "photo", "sky", "unicorn", "rainbow", "color", "pride", "diversity", "spectrum", "refract", "leprechaun", "gold"], + "moji": "🌈" + }, + "raised_hand": { + "unicode": "270B", + "unicode_alternates": [], + "name": "raised hand", + "shortname": ":raised_hand:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman"], + "moji": "✋" + }, + "raised_hands": { + "unicode": "1F64C", + "unicode_alternates": [], + "name": "person raising both hands in celebration", + "shortname": ":raised_hands:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["gesture", "hooray", "winning", "woot", "yay", "banzai"], + "moji": "🙌" + }, + "raising_hand": { + "unicode": "1F64B", + "unicode_alternates": [], + "name": "happy person raising one hand", + "shortname": ":raising_hand:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girl", "woman", "hand", "raise", "notice", "attention", "answer"], + "moji": "🙋" + }, + "ram": { + "unicode": "1F40F", + "unicode_alternates": [], + "name": "ram", + "shortname": ":ram:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "sheep", "ram", "sheep", "male", "horn", "horns"], + "moji": "ðŸ" + }, + "ramen": { + "unicode": "1F35C", + "unicode_alternates": [], + "name": "steaming bowl", + "shortname": ":ramen:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chipsticks", "food", "japanese", "noodle", "ramen", "noodles", "bowl", "steaming", "soup"], + "moji": "ðŸœ" + }, + "rat": { + "unicode": "1F400", + "unicode_alternates": [], + "name": "rat", + "shortname": ":rat:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "mouse", "rat", "rodent", "crooked", "snitch"], + "moji": "ðŸ€" + }, + "recycle": { + "unicode": "267B", + "unicode_alternates": ["267B-FE0F"], + "name": "black universal recycling symbol", + "shortname": ":recycle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "environment", "garbage", "trash"], + "moji": "â™»" + }, + "red_car": { + "unicode": "1F697", + "unicode_alternates": [], + "name": "automobile", + "shortname": ":red_car:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle"], + "moji": "🚗" + }, + "red_circle": { + "unicode": "1F534", + "unicode_alternates": [], + "name": "large red circle", + "shortname": ":red_circle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔴" + }, + "registered": { + "moji": "®", + "unicode": "00AE", + "unicode_alternates": [], + "name": "registered sign", + "shortname": ":registered:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alphabet", "circle"] + }, + "relaxed": { + "unicode": "263A", + "unicode_alternates": ["263A-FE0F"], + "name": "white smiling face", + "shortname": ":relaxed:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blush", "face", "happiness", "massage", "smile"], + "moji": "☺" + }, + "relieved": { + "unicode": "1F60C", + "unicode_alternates": [], + "name": "relieved face", + "shortname": ":relieved:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "happiness", "massage", "phew", "relaxed", "relieved", "satisfied", "phew", "relief"], + "moji": "😌" + }, + "reminder_ribbon": { + "unicode": "1F397", + "unicode_alternates": [], + "name": "reminder ribbon", + "shortname": ":reminder_ribbon:", + "category": "celebration", + "aliases": [], + "aliases_ascii": [], + "keywords": ["awareness"] + }, + "repeat": { + "unicode": "1F501", + "unicode_alternates": [], + "name": "clockwise rightwards and leftwards open circle arr", + "shortname": ":repeat:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["loop", "record"], + "moji": "ðŸ”" + }, + "repeat_one": { + "unicode": "1F502", + "unicode_alternates": [], + "name": "clockwise rightwards and leftwards open circle arr", + "shortname": ":repeat_one:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "loop"], + "moji": "🔂" + }, + "restroom": { + "unicode": "1F6BB", + "unicode_alternates": [], + "name": "restroom", + "shortname": ":restroom:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "woman", "man", "unisex", "bathroom", "restroom", "sign", "shared", "toilet"], + "moji": "🚻" + }, + "revolving_hearts": { + "unicode": "1F49E", + "unicode_alternates": [], + "name": "revolving hearts", + "shortname": ":revolving_hearts:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "revolving", "moving", "circle", "multiple", "lovers"], + "moji": "💞" + }, + "rewind": { + "unicode": "23EA", + "unicode_alternates": [], + "name": "black left-pointing double triangle", + "shortname": ":rewind:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "play"], + "moji": "âª" + }, + "ribbon": { + "unicode": "1F380", + "unicode_alternates": [], + "name": "ribbon", + "shortname": ":ribbon:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bowtie", "decoration", "girl", "pink", "ribbon", "lace", "wrap", "decorate"], + "moji": "🎀" + }, + "rice": { + "unicode": "1F35A", + "unicode_alternates": [], + "name": "cooked rice", + "shortname": ":rice:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "rice", "white", "grain", "food", "bowl"], + "moji": "ðŸš" + }, + "rice_ball": { + "unicode": "1F359", + "unicode_alternates": [], + "name": "rice ball", + "shortname": ":rice_ball:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "japanese", "rice", "ball", "white", "nori", "seaweed", "japanese"], + "moji": "ðŸ™" + }, + "rice_cracker": { + "unicode": "1F358", + "unicode_alternates": [], + "name": "rice cracker", + "shortname": ":rice_cracker:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "japanese", "rice", "cracker", "seaweed", "food", "japanese"], + "moji": "ðŸ˜" + }, + "rice_scene": { + "unicode": "1F391", + "unicode_alternates": [], + "name": "moon viewing ceremony", + "shortname": ":rice_scene:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "moon", "viewing", "observing", "otsukimi", "tsukimi", "rice", "scene", "festival", "autumn"], + "moji": "🎑" + }, + "right_speaker": { + "unicode": "1F568", + "unicode_alternates": [], + "name": "right speaker", + "shortname": ":right_speaker:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "listen", "hear", "noise", "volume"] + }, + "right_speaker_one": { + "unicode": "1F569", + "unicode_alternates": [], + "name": "right speaker with one sound wave", + "shortname": ":right_speaker_one:", + "category": "objects_symbols", + "aliases": [":right_speaker_with_one_sound_wave:"], + "aliases_ascii": [], + "keywords": ["low", "volume"] + }, + "right_speaker_three": { + "unicode": "1F56A", + "unicode_alternates": [], + "name": "right speaker with three sound waves", + "shortname": ":right_speaker_three:", + "category": "objects_symbols", + "aliases": [":right_speaker_with_three_sound_waves:"], + "aliases_ascii": [], + "keywords": ["loud", "high", "volume"] + }, + "ring": { + "unicode": "1F48D", + "unicode_alternates": [], + "name": "ring", + "shortname": ":ring:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["marriage", "propose", "valentines", "wedding"], + "moji": "ðŸ’" + }, + "ringing_bell": { + "unicode": "1F56D", + "unicode_alternates": [], + "name": "ringing bell", + "shortname": ":ringing_bell:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alert", "ding", "volume", "sound", "chime"] + }, + "rocket": { + "unicode": "1F680", + "unicode_alternates": [], + "name": "rocket", + "shortname": ":rocket:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["launch", "ship", "staffmode", "rocket", "space", "spacecraft", "astronaut", "cosmonaut"], + "moji": "🚀" + }, + "roller_coaster": { + "unicode": "1F3A2", + "unicode_alternates": [], + "name": "roller coaster", + "shortname": ":roller_coaster:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["carnival", "fun", "photo", "play", "playground", "roller", "coaster", "amusement", "park", "fair", "ride", "entertainment"], + "moji": "🎢" + }, + "rooster": { + "unicode": "1F413", + "unicode_alternates": [], + "name": "rooster", + "shortname": ":rooster:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "chicken", "nature", "rooster", "cockerel", "cock", "male", "cock-a-doodle-doo", "crowing"], + "moji": "ðŸ“" + }, + "rose": { + "unicode": "1F339", + "unicode_alternates": [], + "name": "rose", + "shortname": ":rose:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flowers", "love", "valentines", "rose", "fragrant", "flower", "thorns", "love", "petals", "romance"], + "moji": "🌹" + }, + "rosette": { + "unicode": "1F3F5", + "unicode_alternates": [], + "name": "rosette", + "shortname": ":rosette:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flower"] + }, + "rosette_black": { + "unicode": "1F3F6", + "unicode_alternates": [], + "name": "black rosette", + "shortname": ":rosette_black:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flower"] + }, + "rotating_light": { + "unicode": "1F6A8", + "unicode_alternates": [], + "name": "police cars revolving light", + "shortname": ":rotating_light:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["911", "ambulance", "emergency", "police", "light", "police", "emergency"], + "moji": "🚨" + }, + "round_pushpin": { + "unicode": "1F4CD", + "unicode_alternates": [], + "name": "round pushpin", + "shortname": ":round_pushpin:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["stationery"], + "moji": "ðŸ“" + }, + "rowboat": { + "unicode": "1F6A3", + "unicode_alternates": [], + "name": "rowboat", + "shortname": ":rowboat:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hobby", "ship", "sports", "water", "boat", "row", "oar", "paddle"], + "moji": "🚣" + }, + "rugby_football": { + "unicode": "1F3C9", + "unicode_alternates": [], + "name": "rugby football", + "shortname": ":rugby_football:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sports", "rugby", "football", "ball", "sport", "team", "england"], + "moji": "ðŸ‰" + }, + "runner": { + "unicode": "1F3C3", + "unicode_alternates": [], + "name": "runner", + "shortname": ":runner:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["exercise", "man", "walking", "run", "runner", "jog", "exercise", "sprint", "race", "dash"], + "moji": "ðŸƒ" + }, + "running_shirt_with_sash": { + "unicode": "1F3BD", + "unicode_alternates": [], + "name": "running shirt with sash", + "shortname": ":running_shirt_with_sash:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["pageant", "play", "running", "run", "shirt", "cloths", "compete", "sports"], + "moji": "🎽" + }, + "sagittarius": { + "unicode": "2650", + "unicode_alternates": ["2650-FE0F"], + "name": "sagittarius", + "shortname": ":sagittarius:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sagittarius", "centaur", "archer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "moji": "â™" + }, + "sailboat": { + "unicode": "26F5", + "unicode_alternates": ["26F5-FE0F"], + "name": "sailboat", + "shortname": ":sailboat:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ship", "transportation"], + "moji": "⛵" + }, + "sake": { + "unicode": "1F376", + "unicode_alternates": [], + "name": "sake bottle and cup", + "shortname": ":sake:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beverage", "drink", "drunk", "wine", "sake", "wine", "rice", "ferment", "alcohol", "japanese", "drink"], + "moji": "ðŸ¶" + }, + "sandal": { + "unicode": "1F461", + "unicode_alternates": [], + "name": "womans sandal", + "shortname": ":sandal:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "shoes"], + "moji": "👡" + }, + "santa": { + "unicode": "1F385", + "unicode_alternates": [], + "name": "father christmas", + "shortname": ":santa:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["christmas", "father christmas", "festival", "male", "man", "xmas", "santa", "saint nick", "jolly", "ho ho ho", "north pole", "presents", "gifts", "naughty", "nice", "sleigh", "father", "christmas", "holiday"], + "moji": "🎅" + }, + "satellite": { + "unicode": "1F4E1", + "unicode_alternates": [], + "name": "satellite antenna", + "shortname": ":satellite:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication"], + "moji": "📡" + }, + "satellite_orbital": { + "unicode": "1F6F0", + "unicode_alternates": [], + "name": "satellite", + "shortname": ":satellite_orbital:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "orbital", "space"] + }, + "saxophone": { + "unicode": "1F3B7", + "unicode_alternates": [], + "name": "saxophone", + "shortname": ":saxophone:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["instrument", "music", "saxophone", "sax", "music", "instrument", "woodwind"], + "moji": "🎷" + }, + "school": { + "unicode": "1F3EB", + "unicode_alternates": [], + "name": "school", + "shortname": ":school:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["building", "school", "university", "elementary", "middle", "high", "college", "teach", "education"], + "moji": "ðŸ«" + }, + "school_satchel": { + "unicode": "1F392", + "unicode_alternates": [], + "name": "school satchel", + "shortname": ":school_satchel:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bag", "education", "student", "school", "satchel", "backpack", "bag", "packing", "pack", "hike", "education", "adventure", "travel", "sightsee"], + "moji": "🎒" + }, + "scissors": { + "unicode": "2702", + "unicode_alternates": ["2702-FE0F"], + "name": "black scissors", + "shortname": ":scissors:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cut", "stationery"], + "moji": "✂" + }, + "scorpius": { + "unicode": "264F", + "unicode_alternates": ["264F-FE0F"], + "name": "scorpius", + "shortname": ":scorpius:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["scorpius", "scorpion", "scorpio", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"], + "moji": "â™" + }, + "scream": { + "unicode": "1F631", + "unicode_alternates": [], + "name": "face screaming in fear", + "shortname": ":scream:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "munch", "scream", "painting", "artist", "alien"], + "moji": "😱" + }, + "scream_cat": { + "unicode": "1F640", + "unicode_alternates": [], + "name": "weary cat face", + "shortname": ":scream_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "munch", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted", "scream", "painting", "artist"], + "moji": "🙀" + }, + "scroll": { + "unicode": "1F4DC", + "unicode_alternates": [], + "name": "scroll", + "shortname": ":scroll:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["documents"], + "moji": "📜" + }, + "seat": { + "unicode": "1F4BA", + "unicode_alternates": [], + "name": "seat", + "shortname": ":seat:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sit"], + "moji": "💺" + }, + "secret": { + "unicode": "3299", + "unicode_alternates": ["3299-FE0F"], + "name": "circled ideograph secret", + "shortname": ":secret:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["privacy"], + "moji": "㊙" + }, + "see_no_evil": { + "unicode": "1F648", + "unicode_alternates": [], + "name": "see-no-evil monkey", + "shortname": ":see_no_evil:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "monkey", "nature", "monkey", "see", "eyes", "vision", "sight", "mizaru"], + "moji": "🙈" + }, + "seedling": { + "unicode": "1F331", + "unicode_alternates": [], + "name": "seedling", + "shortname": ":seedling:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["grass", "lawn", "nature", "plant", "seedling", "plant", "new", "start", "grow"], + "moji": "🌱" + }, + "seven": { + "moji": "7ï¸âƒ£", + "unicode": "0037-20E3", + "unicode_alternates": ["0037-FE0F-20E3"], + "name": "digit seven", + "shortname": ":seven:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["7", "blue-square", "numbers", "prime"] + }, + "shaved_ice": { + "unicode": "1F367", + "unicode_alternates": [], + "name": "shaved ice", + "shortname": ":shaved_ice:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["desert", "hot", "shaved", "ice", "dessert", "treat", "syrup", "flavoring"], + "moji": "ðŸ§" + }, + "sheep": { + "unicode": "1F411", + "unicode_alternates": [], + "name": "sheep", + "shortname": ":sheep:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "sheep", "wool", "flock", "follower", "ewe", "female", "lamb"], + "moji": "ðŸ‘" + }, + "shell": { + "unicode": "1F41A", + "unicode_alternates": [], + "name": "spiral shell", + "shortname": ":shell:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beach", "nature", "sea", "shell", "spiral", "beach", "sand", "crab", "nautilus"], + "moji": "ðŸš" + }, + "shield": { + "unicode": "1F6E1", + "unicode_alternates": [], + "name": "shield", + "shortname": ":shield:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["interstate", "route", "sign", "highway", "interstate"] + }, + "ship": { + "unicode": "1F6A2", + "unicode_alternates": [], + "name": "ship", + "shortname": ":ship:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["titanic", "transportation", "ferry", "ship", "boat"], + "moji": "🚢" + }, + "shirt": { + "unicode": "1F455", + "unicode_alternates": [], + "name": "t-shirt", + "shortname": ":shirt:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cloth", "fashion"], + "moji": "👕" + }, + "shopping_bags": { + "unicode": "1F6CD", + "unicode_alternates": [], + "name": "shopping bags", + "shortname": ":shopping_bags:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["purchase", "mall", "buy", "store", "shop"] + }, + "shower": { + "unicode": "1F6BF", + "unicode_alternates": [], + "name": "shower", + "shortname": ":shower:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bath", "clean", "wash", "bathroom", "shower", "soap", "water", "clean", "shampoo", "lather"], + "moji": "🚿" + }, + "signal_strength": { + "unicode": "1F4F6", + "unicode_alternates": [], + "name": "antenna with bars", + "shortname": ":signal_strength:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "📶" + }, + "six": { + "moji": "6ï¸âƒ£", + "unicode": "0036-20E3", + "unicode_alternates": ["0036-FE0F-20E3"], + "name": "digit six", + "shortname": ":six:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["6", "blue-square", "numbers"] + }, + "six_pointed_star": { + "unicode": "1F52F", + "unicode_alternates": [], + "name": "six pointed star with middle dot", + "shortname": ":six_pointed_star:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["purple-square"], + "moji": "🔯" + }, + "ski": { + "unicode": "1F3BF", + "unicode_alternates": [], + "name": "ski and ski boot", + "shortname": ":ski:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cold", "sports", "winter", "ski", "downhill", "cross-country", "poles", "snow", "winter", "mountain", "alpine", "powder", "slalom", "freestyle"], + "moji": "🎿" + }, + "skull": { + "unicode": "1F480", + "unicode_alternates": [], + "name": "skull", + "shortname": ":skull:", + "category": "emoticons", + "aliases": [":skeleton:"], + "aliases_ascii": [], + "keywords": ["dead", "skeleton", "dying"], + "moji": "💀" + }, + "sleeping": { + "unicode": "1F634", + "unicode_alternates": [], + "name": "sleeping face", + "shortname": ":sleeping:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "sleepy", "tired", "sleep", "sleepy", "sleeping", "snore"], + "moji": "😴" + }, + "sleeping_accommodation": { + "unicode": "1F6CC", + "unicode_alternates": [], + "name": "sleeping accommodation", + "shortname": ":sleeping_accommodation:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["hotel", "motel", "rest"] + }, + "sleepy": { + "unicode": "1F62A", + "unicode_alternates": [], + "name": "sleepy face", + "shortname": ":sleepy:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "rest", "tired", "sleepy", "tired", "exhausted"], + "moji": "😪" + }, + "slight_frown": { + "unicode": "1F641", + "unicode_alternates": [], + "name": "slightly frowning face", + "shortname": ":slight_frown:", + "category": "people", + "aliases": [":slightly_frowning_face:"], + "aliases_ascii": [], + "keywords": ["slight", "frown", "unhappy", "disappointed"] + }, + "slight_smile": { + "unicode": "1F642", + "unicode_alternates": [], + "name": "slightly smiling face", + "shortname": ":slight_smile:", + "category": "people", + "aliases": [":slightly_smiling_face:"], + "aliases_ascii": [], + "keywords": ["slight", "smile", "happy"] + }, + "slot_machine": { + "unicode": "1F3B0", + "unicode_alternates": [], + "name": "slot machine", + "shortname": ":slot_machine:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bet", "gamble", "vegas", "slot", "machine", "gamble", "one-armed bandit", "slots", "luck"], + "moji": "🎰" + }, + "small_blue_diamond": { + "unicode": "1F539", + "unicode_alternates": [], + "name": "small blue diamond", + "shortname": ":small_blue_diamond:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔹" + }, + "small_orange_diamond": { + "unicode": "1F538", + "unicode_alternates": [], + "name": "small orange diamond", + "shortname": ":small_orange_diamond:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔸" + }, + "small_red_triangle": { + "unicode": "1F53A", + "unicode_alternates": [], + "name": "up-pointing red triangle", + "shortname": ":small_red_triangle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔺" + }, + "small_red_triangle_down": { + "unicode": "1F53B", + "unicode_alternates": [], + "name": "down-pointing red triangle", + "shortname": ":small_red_triangle_down:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔻" + }, + "smile": { + "unicode": "1F604", + "unicode_alternates": [], + "name": "smiling face with open mouth and smiling eyes", + "shortname": ":smile:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":)", ":-)", "=]", "=)", ":]"], + "keywords": ["face", "funny", "haha", "happy", "joy", "laugh", "smile", "smiley", "smiling"], + "moji": "😄" + }, + "smile_cat": { + "unicode": "1F638", + "unicode_alternates": [], + "name": "grinning cat face with smiling eyes", + "shortname": ":smile_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "cat", "smile", "grin", "grinning"], + "moji": "😸" + }, + "smiley": { + "unicode": "1F603", + "unicode_alternates": [], + "name": "smiling face with open mouth", + "shortname": ":smiley:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":D", ":-D", "=D"], + "keywords": ["face", "haha", "happy", "joy", "smiling", "smile", "smiley"], + "moji": "😃" + }, + "smiley_cat": { + "unicode": "1F63A", + "unicode_alternates": [], + "name": "smiling cat face with open mouth", + "shortname": ":smiley_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "happy", "smile", "smiley", "cat", "happy"], + "moji": "😺" + }, + "smiling_imp": { + "unicode": "1F608", + "unicode_alternates": [], + "name": "smiling face with horns", + "shortname": ":smiling_imp:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["devil", "horns", "horns", "devil", "impish", "trouble"], + "moji": "😈" + }, + "smirk": { + "unicode": "1F60F", + "unicode_alternates": [], + "name": "smirking face", + "shortname": ":smirk:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mean", "prank", "smile", "smug", "smirking", "smirk", "smug", "smile", "half-smile", "conceited"], + "moji": "ðŸ˜" + }, + "smirk_cat": { + "unicode": "1F63C", + "unicode_alternates": [], + "name": "cat face with wry smile", + "shortname": ":smirk_cat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cats", "smirk", "smirking", "wry", "confident", "confidence"], + "moji": "😼" + }, + "smoking": { + "unicode": "1F6AC", + "unicode_alternates": [], + "name": "smoking symbol", + "shortname": ":smoking:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cigarette", "kills", "tobacco", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"], + "moji": "🚬" + }, + "snail": { + "unicode": "1F40C", + "unicode_alternates": [], + "name": "snail", + "shortname": ":snail:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "shell", "slow", "snail", "slow", "escargot", "french", "appetizer"], + "moji": "ðŸŒ" + }, + "snake": { + "unicode": "1F40D", + "unicode_alternates": [], + "name": "snake", + "shortname": ":snake:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "evil"], + "moji": "ðŸ" + }, + "snowboarder": { + "unicode": "1F3C2", + "unicode_alternates": [], + "name": "snowboarder", + "shortname": ":snowboarder:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sports", "winter", "snow", "boarding", "sports", "freestyle", "halfpipe", "board", "mountain", "alpine", "winter"], + "moji": "ðŸ‚" + }, + "snowflake": { + "unicode": "2744", + "unicode_alternates": ["2744-FE0F"], + "name": "snowflake", + "shortname": ":snowflake:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas", "snowflake", "snow", "frozen", "droplet", "ice", "crystal", "cold", "chilly", "winter", "unique", "special", "below zero", "elsa"], + "moji": "â„" + }, + "snowman": { + "unicode": "26C4", + "unicode_alternates": ["26C4-FE0F"], + "name": "snowman without snow", + "shortname": ":snowman:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas"], + "moji": "⛄" + }, + "sob": { + "unicode": "1F62D", + "unicode_alternates": [], + "name": "loudly crying face", + "shortname": ":sob:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cry", "face", "sad", "tears", "upset", "cry", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"], + "moji": "ðŸ˜" + }, + "soccer": { + "unicode": "26BD", + "unicode_alternates": ["26BD-FE0F"], + "name": "soccer ball", + "shortname": ":soccer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["balls", "fifa", "football", "sports", "european", "football"], + "moji": "âš½" + }, + "soon": { + "unicode": "1F51C", + "unicode_alternates": [], + "name": "soon with rightwards arrow above", + "shortname": ":soon:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arrow", "words"], + "moji": "🔜" + }, + "sos": { + "unicode": "1F198", + "unicode_alternates": [], + "name": "squared sos", + "shortname": ":sos:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["emergency", "help", "red-square", "words"], + "moji": "🆘" + }, + "sound": { + "unicode": "1F509", + "unicode_alternates": [], + "name": "speaker with one sound wave", + "shortname": ":sound:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["speaker", "volume"], + "moji": "🔉" + }, + "space_invader": { + "unicode": "1F47E", + "unicode_alternates": [], + "name": "alien monster", + "shortname": ":space_invader:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arcade", "game"], + "moji": "👾" + }, + "spades": { + "unicode": "2660", + "unicode_alternates": ["2660-FE0F"], + "name": "black spade suit", + "shortname": ":spades:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cards", "poker"], + "moji": "â™ " + }, + "spaghetti": { + "unicode": "1F35D", + "unicode_alternates": [], + "name": "spaghetti", + "shortname": ":spaghetti:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "italian", "noodle", "spaghetti", "noodles", "tomato", "sauce", "italian"], + "moji": "ðŸ" + }, + "sparkle": { + "unicode": "2747", + "unicode_alternates": ["2747-FE0F"], + "name": "sparkle", + "shortname": ":sparkle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["green-square", "stars"], + "moji": "â‡" + }, + "sparkler": { + "unicode": "1F387", + "unicode_alternates": [], + "name": "firework sparkler", + "shortname": ":sparkler:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "shine", "stars"], + "moji": "🎇" + }, + "sparkles": { + "unicode": "2728", + "unicode_alternates": [], + "name": "sparkles", + "shortname": ":sparkles:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cool", "shine", "shiny", "stars"], + "moji": "✨" + }, + "sparkling_heart": { + "unicode": "1F496", + "unicode_alternates": [], + "name": "sparkling heart", + "shortname": ":sparkling_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines"], + "moji": "💖" + }, + "speak_no_evil": { + "unicode": "1F64A", + "unicode_alternates": [], + "name": "speak-no-evil monkey", + "shortname": ":speak_no_evil:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "monkey", "monkey", "mouth", "talk", "say", "words", "verbal", "verbalize", "oral", "iwazaru"], + "moji": "🙊" + }, + "speaker": { + "unicode": "1F508", + "unicode_alternates": [], + "name": "speaker", + "shortname": ":speaker:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sound", "listen", "hear", "noise"] + }, + "speaking_head": { + "unicode": "1F5E3", + "unicode_alternates": [], + "name": "speaking head in silhouette", + "shortname": ":speaking_head:", + "category": "objects_symbols", + "aliases": [":speaking_head_in_silhouette:"], + "aliases_ascii": [], + "keywords": ["talk"] + }, + "speech_balloon": { + "unicode": "1F4AC", + "unicode_alternates": [], + "name": "speech balloon", + "shortname": ":speech_balloon:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bubble", "words", "speech", "balloon", "talk", "conversation", "communication", "comic", "dialogue"], + "moji": "💬" + }, + "speech_left": { + "unicode": "1F5E8", + "unicode_alternates": [], + "name": "left speech bubble", + "shortname": ":speech_left:", + "category": "objects_symbols", + "aliases": [":left_speech_bubble:"], + "aliases_ascii": [], + "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + }, + "speech_right": { + "unicode": "1F5E9", + "unicode_alternates": [], + "name": "right speech bubble", + "shortname": ":speech_right:", + "category": "objects_symbols", + "aliases": [":right_speech_bubble:"], + "aliases_ascii": [], + "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + }, + "speech_three": { + "unicode": "1F5EB", + "unicode_alternates": [], + "name": "three speech bubbles", + "shortname": ":speech_three:", + "category": "objects_symbols", + "aliases": [":three_speech_bubbles:"], + "aliases_ascii": [], + "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + }, + "speech_two": { + "unicode": "1F5EA", + "unicode_alternates": [], + "name": "two speech bubbles", + "shortname": ":speech_two:", + "category": "objects_symbols", + "aliases": [":two_speech_bubbles:"], + "aliases_ascii": [], + "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"] + }, + "speedboat": { + "unicode": "1F6A4", + "unicode_alternates": [], + "name": "speedboat", + "shortname": ":speedboat:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ship", "transportation", "vehicle", "motor", "speed", "ski", "power", "boat"], + "moji": "🚤" + }, + "spider": { + "unicode": "1F577", + "unicode_alternates": [], + "name": "spider", + "shortname": ":spider:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["arachnid", "eight-legged"] + }, + "spider_web": { + "unicode": "1F578", + "unicode_alternates": [], + "name": "spider web", + "shortname": ":spider_web:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cobweb"] + }, + "spy": { + "unicode": "1F575", + "unicode_alternates": [], + "name": "sleuth or spy", + "shortname": ":spy:", + "category": "people", + "aliases": [":sleuth_or_spy:"], + "aliases_ascii": [], + "keywords": ["pi", "undercover", "investigator"] + }, + "stadium": { + "unicode": "1F3DF", + "unicode_alternates": [], + "name": "stadium", + "shortname": ":stadium:", + "category": "travel_places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sport", "event", "concert", "convention", "game"] + }, + "star": { + "unicode": "2B50", + "unicode_alternates": ["2B50-FE0F"], + "name": "white medium star", + "shortname": ":star:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "yellow"], + "moji": "â" + }, + "star2": { + "unicode": "1F31F", + "unicode_alternates": [], + "name": "glowing star", + "shortname": ":star2:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "sparkle", "glow", "glowing", "star", "five", "points", "classic"], + "moji": "🌟" + }, + "stars": { + "unicode": "1F320", + "unicode_alternates": [], + "name": "shooting star", + "shortname": ":stars:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["night", "photo", "shooting", "shoot", "star", "sky", "night", "comet", "meteoroid"], + "moji": "🌠" + }, + "station": { + "unicode": "1F689", + "unicode_alternates": [], + "name": "station", + "shortname": ":station:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["public", "transportation", "vehicle", "station", "train", "subway"], + "moji": "🚉" + }, + "statue_of_liberty": { + "unicode": "1F5FD", + "unicode_alternates": [], + "name": "statue of liberty", + "shortname": ":statue_of_liberty:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["american", "newyork"], + "moji": "🗽" + }, + "steam_locomotive": { + "unicode": "1F682", + "unicode_alternates": [], + "name": "steam locomotive", + "shortname": ":steam_locomotive:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["train", "transportation", "vehicle", "locomotive", "steam", "train", "engine"], + "moji": "🚂" + }, + "stereo": { + "unicode": "1F4FE", + "unicode_alternates": [], + "name": "portable stereo", + "shortname": ":stereo:", + "category": "objects_symbols", + "aliases": [":portable_stereo:"], + "aliases_ascii": [], + "keywords": ["communication", "music", "program", "boom", "box"] + }, + "stew": { + "unicode": "1F372", + "unicode_alternates": [], + "name": "pot of food", + "shortname": ":stew:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "meat", "stew", "hearty", "soup", "thick", "hot", "pot"], + "moji": "ðŸ²" + }, + "stock_chart": { + "unicode": "1F5E0", + "unicode_alternates": [], + "name": "stock chart", + "shortname": ":stock_chart:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["graph", "presentation", "stats", "business"] + }, + "straight_ruler": { + "unicode": "1F4CF", + "unicode_alternates": [], + "name": "straight ruler", + "shortname": ":straight_ruler:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["stationery"], + "moji": "ðŸ“" + }, + "strawberry": { + "unicode": "1F353", + "unicode_alternates": [], + "name": "strawberry", + "shortname": ":strawberry:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "strawberry", "short", "cake", "berry"], + "moji": "ðŸ“" + }, + "stuck_out_tongue": { + "unicode": "1F61B", + "unicode_alternates": [], + "name": "face with stuck-out tongue", + "shortname": ":stuck_out_tongue:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [":P", ":-P", "=P", ":-p", ":p", "=p", ":-Þ", ":Þ", ":þ", ":-þ", ":-b", ":b", "d:"], + "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "silly", "playful", "cheeky"], + "moji": "😛" + }, + "stuck_out_tongue_closed_eyes": { + "unicode": "1F61D", + "unicode_alternates": [], + "name": "face with stuck-out tongue and tightly-closed eyes", + "shortname": ":stuck_out_tongue_closed_eyes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "mischievous", "playful", "prank", "tongue", "kidding", "silly", "playful", "ecstatic"], + "moji": "ðŸ˜" + }, + "stuck_out_tongue_winking_eye": { + "unicode": "1F61C", + "unicode_alternates": [], + "name": "face with stuck-out tongue and winking eye", + "shortname": ":stuck_out_tongue_winking_eye:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [">:P", "X-P", "x-p"], + "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "wink", "winking", "kidding", "silly", "playful", "crazy"], + "moji": "😜" + }, + "sun_with_face": { + "unicode": "1F31E", + "unicode_alternates": [], + "name": "sun with face", + "shortname": ":sun_with_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["morning", "sun", "anthropomorphic", "face", "sky"], + "moji": "🌞" + }, + "sunflower": { + "unicode": "1F33B", + "unicode_alternates": [], + "name": "sunflower", + "shortname": ":sunflower:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "sunflower", "sun", "flower", "seeds", "yellow"], + "moji": "🌻" + }, + "sunglasses": { + "unicode": "1F60E", + "unicode_alternates": [], + "name": "smiling face with sunglasses", + "shortname": ":sunglasses:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["B-)", "B)", "8)", "8-)", "B-D", "8-D"], + "keywords": ["cool", "face", "smiling", "sunglasses", "sun", "glasses", "sunny", "cool", "smooth"], + "moji": "😎" + }, + "sunny": { + "unicode": "2600", + "unicode_alternates": ["2600-FE0F"], + "name": "black sun with rays", + "shortname": ":sunny:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["brightness", "weather"] + }, + "sunrise": { + "unicode": "1F305", + "unicode_alternates": [], + "name": "sunrise", + "shortname": ":sunrise:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["morning", "photo", "vacation", "view", "sunrise", "sun", "morning", "color", "sky"], + "moji": "🌅" + }, + "sunrise_over_mountains": { + "unicode": "1F304", + "unicode_alternates": [], + "name": "sunrise over mountains", + "shortname": ":sunrise_over_mountains:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["photo", "vacation", "view", "sunrise", "sun", "morning", "mountain", "rural", "color", "sky"], + "moji": "🌄" + }, + "surfer": { + "unicode": "1F3C4", + "unicode_alternates": [], + "name": "surfer", + "shortname": ":surfer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ocean", "sea", "sports", "surfer", "surf", "wave", "ocean", "ride", "swell"], + "moji": "ðŸ„" + }, + "sushi": { + "unicode": "1F363", + "unicode_alternates": [], + "name": "sushi", + "shortname": ":sushi:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "japanese", "sushi", "fish", "raw", "nigiri", "japanese"], + "moji": "ðŸ£" + }, + "suspension_railway": { + "unicode": "1F69F", + "unicode_alternates": [], + "name": "suspension railway", + "shortname": ":suspension_railway:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "suspension", "railway", "rail", "train", "transportation"], + "moji": "🚟" + }, + "sweat": { + "unicode": "1F613", + "unicode_alternates": [], + "name": "face with cold sweat", + "shortname": ":sweat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["':(", "':-(", "'=("], + "keywords": ["cold", "sweat", "sick", "anxious", "worried", "clammy", "diaphoresis", "face", "hot"], + "moji": "😓" + }, + "sweat_drops": { + "unicode": "1F4A6", + "unicode_alternates": [], + "name": "splashing sweat symbol", + "shortname": ":sweat_drops:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["water"], + "moji": "💦" + }, + "sweat_smile": { + "unicode": "1F605", + "unicode_alternates": [], + "name": "smiling face with open mouth and cold sweat", + "shortname": ":sweat_smile:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": ["':)", "':-)", "'=)", "':D", "':-D", "'=D"], + "keywords": ["face", "happy", "hot", "smiling", "cold", "sweat", "perspiration"], + "moji": "😅" + }, + "sweet_potato": { + "unicode": "1F360", + "unicode_alternates": [], + "name": "roasted sweet potato", + "shortname": ":sweet_potato:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "nature", "sweet", "potato", "potassium", "roasted", "roast"], + "moji": "ðŸ " + }, + "swimmer": { + "unicode": "1F3CA", + "unicode_alternates": [], + "name": "swimmer", + "shortname": ":swimmer:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sports", "swimmer", "swim", "water", "pool", "laps", "freestyle", "butterfly", "breaststroke", "backstroke"], + "moji": "ðŸŠ" + }, + "symbols": { + "unicode": "1F523", + "unicode_alternates": [], + "name": "input symbol for symbols", + "shortname": ":symbols:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "🔣" + }, + "syringe": { + "unicode": "1F489", + "unicode_alternates": [], + "name": "syringe", + "shortname": ":syringe:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blood", "drugs", "health", "hospital", "medicine", "needle"], + "moji": "💉" + }, + "tada": { + "unicode": "1F389", + "unicode_alternates": [], + "name": "party popper", + "shortname": ":tada:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["contulations", "party", "party", "popper", "tada", "celebration", "victory", "announcement", "climax", "congratulations"], + "moji": "🎉" + }, + "tanabata_tree": { + "unicode": "1F38B", + "unicode_alternates": [], + "name": "tanabata tree", + "shortname": ":tanabata_tree:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "plant", "tanabata", "tree", "festival", "star", "wish", "holiday"], + "moji": "🎋" + }, + "tangerine": { + "unicode": "1F34A", + "unicode_alternates": [], + "name": "tangerine", + "shortname": ":tangerine:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "tangerine", "citrus", "orange"], + "moji": "ðŸŠ" + }, + "taurus": { + "unicode": "2649", + "unicode_alternates": ["2649-FE0F"], + "name": "taurus", + "shortname": ":taurus:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["purple-square", "sign", "taurus", "bull", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"], + "moji": "♉" + }, + "taxi": { + "unicode": "1F695", + "unicode_alternates": [], + "name": "taxi", + "shortname": ":taxi:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "transportation", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"], + "moji": "🚕" + }, + "tea": { + "unicode": "1F375", + "unicode_alternates": [], + "name": "teacup without handle", + "shortname": ":tea:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bowl", "breakfast", "british", "drink", "green", "tea", "leaf", "drink", "teacup", "hot", "beverage"], + "moji": "ðŸµ" + }, + "telephone": { + "unicode": "260E", + "unicode_alternates": ["260E-FE0F"], + "name": "black telephone", + "shortname": ":telephone:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "dial", "technology"], + "moji": "☎" + }, + "telephone_black": { + "unicode": "1F57F", + "unicode_alternates": [], + "name": "black touchtone telephone", + "shortname": ":telephone_black:", + "category": "objects_symbols", + "aliases": [":black_touchtone_telephone:"], + "aliases_ascii": [], + "keywords": ["communication", "dial", "technology"] + }, + "telephone_receiver": { + "unicode": "1F4DE", + "unicode_alternates": [], + "name": "telephone receiver", + "shortname": ":telephone_receiver:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["communication", "dial", "technology"], + "moji": "📞" + }, + "telephone_white": { + "unicode": "1F57E", + "unicode_alternates": [], + "name": "white touchtone telephone", + "shortname": ":telephone_white:", + "category": "objects_symbols", + "aliases": [":white_touchtone_telephone:"], + "aliases_ascii": [], + "keywords": ["communication", "dial", "technology"] + }, + "telescope": { + "unicode": "1F52D", + "unicode_alternates": [], + "name": "telescope", + "shortname": ":telescope:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["space", "stars"], + "moji": "ðŸ”" + }, + "tennis": { + "unicode": "1F3BE", + "unicode_alternates": [], + "name": "tennis racquet and ball", + "shortname": ":tennis:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["balls", "green", "sports", "tennis", "racket", "racquet", "ball", "game", "net", "court", "love"], + "moji": "🎾" + }, + "tent": { + "unicode": "26FA", + "unicode_alternates": ["26FA-FE0F"], + "name": "tent", + "shortname": ":tent:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["camp", "outdoors", "photo"], + "moji": "⛺" + }, + "thermometer": { + "unicode": "1F321", + "unicode_alternates": [], + "name": "thermometer", + "shortname": ":thermometer:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["temperature"] + }, + "thought_balloon": { + "unicode": "1F4AD", + "unicode_alternates": [], + "name": "thought balloon", + "shortname": ":thought_balloon:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bubble", "cloud", "speech", "thought", "balloon", "comic", "think", "day dream", "wonder"], + "moji": "ðŸ’" + }, + "thought_left": { + "unicode": "1F5EC", + "unicode_alternates": [], + "name": "left thought bubble", + "shortname": ":thought_left:", + "category": "objects_symbols", + "aliases": [":left_thought_bubble:"], + "aliases_ascii": [], + "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"] + }, + "thought_right": { + "unicode": "1F5ED", + "unicode_alternates": [], + "name": "right thought bubble", + "shortname": ":thought_right:", + "category": "objects_symbols", + "aliases": [":right_thought_bubble:"], + "aliases_ascii": [], + "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"] + }, + "three": { + "moji": "3ï¸âƒ£", + "unicode": "0033-20E3", + "unicode_alternates": ["0033-FE0F-20E3"], + "name": "digit three", + "shortname": ":three:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["3", "blue-square", "numbers", "prime"] + }, + "thumbs_down_reverse": { + "unicode": "1F593", + "unicode_alternates": [], + "name": "reversed thumbs down sign", + "shortname": ":thumbs_down_reverse:", + "category": "people", + "aliases": [":reversed_thumbs_down_sign:"], + "aliases_ascii": [], + "keywords": ["hand", "no", "-1"] + }, + "thumbs_up_reverse": { + "unicode": "1F592", + "unicode_alternates": [], + "name": "reversed thumbs up sign", + "shortname": ":thumbs_up_reverse:", + "category": "people", + "aliases": [":reversed_thumbs_up_sign:"], + "aliases_ascii": [], + "keywords": ["cool", "hand", "like", "yes", "+1"] + }, + "thumbsdown": { + "unicode": "1F44E", + "unicode_alternates": [], + "name": "thumbs down sign", + "shortname": ":thumbsdown:", + "category": "emoticons", + "aliases": [":-1:"], + "aliases_ascii": [], + "keywords": ["hand", "no"], + "moji": "👎" + }, + "thumbsup": { + "unicode": "1F44D", + "unicode_alternates": [], + "name": "thumbs up sign", + "shortname": ":thumbsup:", + "category": "emoticons", + "aliases": [":+1:"], + "aliases_ascii": [], + "keywords": ["cool", "hand", "like", "yes"], + "moji": "ðŸ‘" + }, + "ticket": { + "unicode": "1F3AB", + "unicode_alternates": [], + "name": "ticket", + "shortname": ":ticket:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["concert", "event", "pass", "ticket", "show", "entertainment", "stub", "admission", "proof", "purchase"], + "moji": "🎫" + }, + "tickets": { + "unicode": "1F39F", + "unicode_alternates": [], + "name": "admission tickets", + "shortname": ":tickets:", + "category": "activity", + "aliases": [":admission_tickets:"], + "aliases_ascii": [], + "keywords": ["concert", "event", "pass", "show", "entertainment", "stub", "proof", "purchase"] + }, + "tiger": { + "unicode": "1F42F", + "unicode_alternates": [], + "name": "tiger face", + "shortname": ":tiger:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal"], + "moji": "ðŸ¯" + }, + "tiger2": { + "unicode": "1F405", + "unicode_alternates": [], + "name": "tiger", + "shortname": ":tiger2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "tiger", "cat", "striped", "tony", "tigger", "hobs"], + "moji": "ðŸ…" + }, + "tired_face": { + "unicode": "1F62B", + "unicode_alternates": [], + "name": "tired face", + "shortname": ":tired_face:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "frustrated", "sick", "upset", "whine", "exhausted", "sleepy", "tired"], + "moji": "😫" + }, + "toilet": { + "unicode": "1F6BD", + "unicode_alternates": [], + "name": "toilet", + "shortname": ":toilet:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["restroom", "wc", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"], + "moji": "🚽" + }, + "tokyo_tower": { + "unicode": "1F5FC", + "unicode_alternates": [], + "name": "tokyo tower", + "shortname": ":tokyo_tower:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japan", "photo"], + "moji": "🗼" + }, + "tomato": { + "unicode": "1F345", + "unicode_alternates": [], + "name": "tomato", + "shortname": ":tomato:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "nature", "vegetable", "tomato", "fruit", "sauce", "italian"], + "moji": "ðŸ…" + }, + "tongue": { + "unicode": "1F445", + "unicode_alternates": [], + "name": "tongue", + "shortname": ":tongue:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mouth", "playful", "tongue", "mouth", "taste", "buds", "food", "silly", "playful", "tease", "kiss", "french kiss", "lick", "tasty", "playfulness", "silliness", "intimacy"], + "moji": "👅" + }, + "tools": { + "unicode": "1F6E0", + "unicode_alternates": [], + "name": "hammer and wrench", + "shortname": ":tools:", + "category": "objects_symbols", + "aliases": [":hammer_and_wrench:"], + "aliases_ascii": [], + "keywords": ["tools"] + }, + "top": { + "unicode": "1F51D", + "unicode_alternates": [], + "name": "top with upwards arrow above", + "shortname": ":top:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "words"], + "moji": "ðŸ”" + }, + "tophat": { + "unicode": "1F3A9", + "unicode_alternates": [], + "name": "top hat", + "shortname": ":tophat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["classy", "gentleman", "magic", "top", "hat", "cap", "beaver", "high", "tall", "stove", "pipe", "chimney", "topper", "london", "period piece", "magic", "magician"], + "moji": "🎩" + }, + "trackball": { + "unicode": "1F5B2", + "unicode_alternates": [], + "name": "trackball", + "shortname": ":trackball:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["input", "device", "gadget"] + }, + "tractor": { + "unicode": "1F69C", + "unicode_alternates": [], + "name": "tractor", + "shortname": ":tractor:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["agriculture", "car", "farming", "vehicle", "tractor", "farm", "construction", "machine", "digger"], + "moji": "🚜" + }, + "traffic_light": { + "unicode": "1F6A5", + "unicode_alternates": [], + "name": "horizontal traffic light", + "shortname": ":traffic_light:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["traffic", "transportation", "traffic", "light", "stop", "go", "yield", "horizontal"], + "moji": "🚥" + }, + "train": { + "unicode": "1F68B", + "unicode_alternates": [], + "name": "Tram Car", + "shortname": ":train:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["tram", "rail"] + }, + "train2": { + "unicode": "1F686", + "unicode_alternates": [], + "name": "train", + "shortname": ":train2:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "train", "locomotive", "rail"], + "moji": "🚆" + }, + "train_diesel": { + "unicode": "1F6F2", + "unicode_alternates": [], + "name": "diesel locomotive", + "shortname": ":train_diesel:", + "category": "travel_places", + "aliases": [":diesel_locomotive:"], + "aliases_ascii": [], + "keywords": ["train", "transportation", "engine", "rail"] + }, + "tram": { + "unicode": "1F68A", + "unicode_alternates": [], + "name": "tram", + "shortname": ":tram:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "vehicle", "tram", "transportation", "transport"], + "moji": "🚊" + }, + "triangle_round": { + "unicode": "1F6C6", + "unicode_alternates": [], + "name": "triangle with rounded corners", + "shortname": ":triangle_round:", + "category": "objects_symbols", + "aliases": [":triangle_with_rounded_corners:"], + "aliases_ascii": [], + "keywords": ["caution", "warning", "alert"] + }, + "triangular_flag_on_post": { + "unicode": "1F6A9", + "unicode_alternates": [], + "name": "triangular flag on post", + "shortname": ":triangular_flag_on_post:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["triangle", "triangular", "flag", "golf", "post", "flagpole"], + "moji": "🚩" + }, + "triangular_ruler": { + "unicode": "1F4D0", + "unicode_alternates": [], + "name": "triangular ruler", + "shortname": ":triangular_ruler:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["architect", "math", "sketch", "stationery"], + "moji": "ðŸ“" + }, + "trident": { + "unicode": "1F531", + "unicode_alternates": [], + "name": "trident emblem", + "shortname": ":trident:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["spear", "weapon"], + "moji": "🔱" + }, + "triumph": { + "unicode": "1F624", + "unicode_alternates": [], + "name": "face with look of triumph", + "shortname": ":triumph:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "gas", "phew", "triumph", "steam", "breath"], + "moji": "😤" + }, + "trolleybus": { + "unicode": "1F68E", + "unicode_alternates": [], + "name": "trolleybus", + "shortname": ":trolleybus:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bart", "transportation", "vehicle", "trolley", "bus", "city", "transport", "transportation"], + "moji": "🚎" + }, + "trophy": { + "unicode": "1F3C6", + "unicode_alternates": [], + "name": "trophy", + "shortname": ":trophy:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "trophy", "first", "show", "place", "win", "reward", "achievement", "medal"], + "moji": "ðŸ†" + }, + "tropical_drink": { + "unicode": "1F379", + "unicode_alternates": [], + "name": "tropical drink", + "shortname": ":tropical_drink:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["beverage", "tropical", "drink", "mixed", "pineapple", "coconut", "pina", "fruit", "umbrella"], + "moji": "ðŸ¹" + }, + "tropical_fish": { + "unicode": "1F420", + "unicode_alternates": [], + "name": "tropical fish", + "shortname": ":tropical_fish:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "swim"], + "moji": "ðŸ " + }, + "truck": { + "unicode": "1F69A", + "unicode_alternates": [], + "name": "delivery truck", + "shortname": ":truck:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["cars", "transportation", "truck", "delivery", "package"], + "moji": "🚚" + }, + "trumpet": { + "unicode": "1F3BA", + "unicode_alternates": [], + "name": "trumpet", + "shortname": ":trumpet:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["brass", "music", "trumpet", "brass", "music", "instrument"], + "moji": "🎺" + }, + "tulip": { + "unicode": "1F337", + "unicode_alternates": [], + "name": "tulip", + "shortname": ":tulip:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["flowers", "nature", "plant", "tulip", "flower", "bulb", "spring", "easter"], + "moji": "🌷" + }, + "turned_ok_hand": { + "unicode": "1F58F", + "unicode_alternates": [], + "name": "turned ok hand sign", + "shortname": ":turned_ok_hand:", + "category": "people", + "aliases": [":turned_ok_hand_sign:"], + "aliases_ascii": [], + "keywords": ["perfect", "okay"] + }, + "turtle": { + "unicode": "1F422", + "unicode_alternates": [], + "name": "turtle", + "shortname": ":turtle:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "slow", "turtle", "shell", "tortoise", "chelonian", "reptile", "slow", "snap", "steady"], + "moji": "ðŸ¢" + }, + "twisted_rightwards_arrows": { + "unicode": "1F500", + "unicode_alternates": [], + "name": "twisted rightwards arrows", + "shortname": ":twisted_rightwards_arrows:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "🔀" + }, + "two": { + "moji": "2ï¸âƒ£", + "unicode": "0032-20E3", + "unicode_alternates": ["0032-FE0F-20E3"], + "name": "digit two", + "shortname": ":two:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["2", "blue-square", "numbers", "prime"] + }, + "two_hearts": { + "unicode": "1F495", + "unicode_alternates": [], + "name": "two hearts", + "shortname": ":two_hearts:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "two", "love", "emotion"], + "moji": "💕" + }, + "two_men_holding_hands": { + "unicode": "1F46C", + "unicode_alternates": [], + "name": "two men holding hands", + "shortname": ":two_men_holding_hands:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bromance", "couple", "friends", "like", "love", "men", "gay", "homosexual", "friends", "hands", "holding", "team", "unity"], + "moji": "👬" + }, + "two_women_holding_hands": { + "unicode": "1F46D", + "unicode_alternates": [], + "name": "two women holding hands", + "shortname": ":two_women_holding_hands:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["couple", "female", "friends", "like", "love", "women", "hands", "girlfriends", "friends", "sisters", "mother", "daughter", "gay", "homosexual", "couple", "unity"], + "moji": "ðŸ‘" + }, + "u5272": { + "unicode": "1F239", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-5272", + "shortname": ":u5272:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "cut", "divide", "kanji", "pink"], + "moji": "🈹" + }, + "u5408": { + "unicode": "1F234", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-5408", + "shortname": ":u5408:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "japanese", "join", "kanji"], + "moji": "🈴" + }, + "u55b6": { + "unicode": "1F23A", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-55b6", + "shortname": ":u55b6:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japanese", "opening hours"], + "moji": "🈺" + }, + "u6307": { + "unicode": "1F22F", + "unicode_alternates": ["1F22F-FE0F"], + "name": "squared cjk unified ideograph-6307", + "shortname": ":u6307:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "green-square", "kanji", "point"], + "moji": "🈯" + }, + "u6708": { + "unicode": "1F237", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-6708", + "shortname": ":u6708:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "japanese", "kanji", "moon", "orange-square"], + "moji": "🈷" + }, + "u6709": { + "unicode": "1F236", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-6709", + "shortname": ":u6709:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "have", "kanji", "orange-square"], + "moji": "🈶" + }, + "u6e80": { + "unicode": "1F235", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-6e80", + "shortname": ":u6e80:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "full", "japanese", "kanji", "red-square"], + "moji": "🈵" + }, + "u7121": { + "unicode": "1F21A", + "unicode_alternates": ["1F21A-FE0F"], + "name": "squared cjk unified ideograph-7121", + "shortname": ":u7121:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "japanese", "kanji", "no", "nothing", "orange-square"], + "moji": "🈚" + }, + "u7533": { + "unicode": "1F238", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-7533", + "shortname": ":u7533:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "japanese", "kanji"], + "moji": "🈸" + }, + "u7981": { + "unicode": "1F232", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-7981", + "shortname": ":u7981:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "forbidden", "japanese", "kanji", "limit", "restricted"], + "moji": "🈲" + }, + "u7a7a": { + "unicode": "1F233", + "unicode_alternates": [], + "name": "squared cjk unified ideograph-7a7a", + "shortname": ":u7a7a:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["chinese", "empty", "japanese", "kanji"], + "moji": "🈳" + }, + "umbrella": { + "unicode": "2614", + "unicode_alternates": ["2614-FE0F"], + "name": "umbrella with rain drops", + "shortname": ":umbrella:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["rain", "weather"], + "moji": "☔" + }, + "unamused": { + "unicode": "1F612", + "unicode_alternates": [], + "name": "unamused face", + "shortname": ":unamused:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["bored", "face", "indifference", "serious", "straight face", "unamused", "not amused", "depressed", "unhappy", "disapprove", "lame"], + "moji": "😒" + }, + "underage": { + "unicode": "1F51E", + "unicode_alternates": [], + "name": "no one under eighteen symbol", + "shortname": ":underage:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["18", "drink", "night", "pub"], + "moji": "🔞" + }, + "unlock": { + "unicode": "1F513", + "unicode_alternates": [], + "name": "open lock", + "shortname": ":unlock:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["privacy", "security"], + "moji": "🔓" + }, + "up": { + "unicode": "1F199", + "unicode_alternates": [], + "name": "squared up with exclamation mark", + "shortname": ":up:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square"], + "moji": "🆙" + }, + "v": { + "unicode": "270C", + "unicode_alternates": ["270C-FE0F"], + "name": "victory hand", + "shortname": ":v:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fingers", "hand", "ohyeah", "peace", "two", "victory"], + "moji": "✌" + }, + "vertical_traffic_light": { + "unicode": "1F6A6", + "unicode_alternates": [], + "name": "vertical traffic light", + "shortname": ":vertical_traffic_light:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["transportation", "traffic", "light", "stop", "go", "yield", "vertical"], + "moji": "🚦" + }, + "vhs": { + "unicode": "1F4FC", + "unicode_alternates": [], + "name": "videocassette", + "shortname": ":vhs:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["oldschool", "record", "video"], + "moji": "📼" + }, + "vibration_mode": { + "unicode": "1F4F3", + "unicode_alternates": [], + "name": "vibration mode", + "shortname": ":vibration_mode:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["orange-square", "phone"], + "moji": "📳" + }, + "video_camera": { + "unicode": "1F4F9", + "unicode_alternates": [], + "name": "video camera", + "shortname": ":video_camera:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["film", "record"], + "moji": "📹" + }, + "video_game": { + "unicode": "1F3AE", + "unicode_alternates": [], + "name": "video game", + "shortname": ":video_game:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["PS4", "console", "controller", "play", "video", "game", "console", "controller", "nintendo", "xbox", "playstation"], + "moji": "🎮" + }, + "violin": { + "unicode": "1F3BB", + "unicode_alternates": [], + "name": "violin", + "shortname": ":violin:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["instrument", "music", "violin", "fiddle", "music", "instrument"], + "moji": "🎻" + }, + "virgo": { + "unicode": "264D", + "unicode_alternates": ["264D-FE0F"], + "name": "virgo", + "shortname": ":virgo:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sign", "virgo", "maiden", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"], + "moji": "â™" + }, + "volcano": { + "unicode": "1F30B", + "unicode_alternates": [], + "name": "volcano", + "shortname": ":volcano:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "photo", "volcano", "lava", "magma", "hot", "explode"], + "moji": "🌋" + }, + "vs": { + "unicode": "1F19A", + "unicode_alternates": [], + "name": "squared vs", + "shortname": ":vs:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["orange-square", "words"], + "moji": "🆚" + }, + "vulcan": { + "unicode": "1F596", + "unicode_alternates": [], + "name": "raised hand with part between middle and ring fingers", + "shortname": ":vulcan:", + "category": "people", + "aliases": [":raised_hand_with_part_between_middle_and_ring_fingers:"], + "aliases_ascii": [], + "keywords": ["vulcan", "spock", "leonard", "nimoy", "star trek", "live long"] + }, + "walking": { + "unicode": "1F6B6", + "unicode_alternates": [], + "name": "pedestrian", + "shortname": ":walking:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["human", "man", "walk", "pedestrian", "stroll", "stride", "foot", "feet"], + "moji": "🚶" + }, + "waning_crescent_moon": { + "unicode": "1F318", + "unicode_alternates": [], + "name": "waning crescent moon symbol", + "shortname": ":waning_crescent_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "crescent", "waning", "sky", "night", "cheese", "phase"], + "moji": "🌘" + }, + "waning_gibbous_moon": { + "unicode": "1F316", + "unicode_alternates": [], + "name": "waning gibbous moon symbol", + "shortname": ":waning_gibbous_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "waning", "gibbous", "sky", "night", "cheese", "phase"], + "moji": "🌖" + }, + "warning": { + "unicode": "26A0", + "unicode_alternates": ["26A0-FE0F"], + "name": "warning sign", + "shortname": ":warning:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["exclamation", "wip"], + "moji": "âš " + }, + "wastebasket": { + "unicode": "1F5D1", + "unicode_alternates": [], + "name": "wastebasket", + "shortname": ":wastebasket:", + "category": "objects_symbols", + "aliases": [], + "aliases_ascii": [], + "keywords": ["trash", "garbage", "dispose"] + }, + "watch": { + "unicode": "231A", + "unicode_alternates": ["231A-FE0F"], + "name": "watch", + "shortname": ":watch:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "time"], + "moji": "⌚" + }, + "water_buffalo": { + "unicode": "1F403", + "unicode_alternates": [], + "name": "water buffalo", + "shortname": ":water_buffalo:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "cow", "nature", "ox", "water", "buffalo", "asia", "bovine", "milk", "dairy"], + "moji": "ðŸƒ" + }, + "watermelon": { + "unicode": "1F349", + "unicode_alternates": [], + "name": "watermelon", + "shortname": ":watermelon:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["food", "fruit", "melon", "watermelon", "summer", "fruit", "large"], + "moji": "ðŸ‰" + }, + "wave": { + "unicode": "1F44B", + "unicode_alternates": [], + "name": "waving hand sign", + "shortname": ":wave:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["farewell", "gesture", "goodbye", "hands", "solong"], + "moji": "👋" + }, + "wavy_dash": { + "unicode": "3030", + "unicode_alternates": [], + "name": "wavy dash", + "shortname": ":wavy_dash:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["draw", "line"], + "moji": "〰" + }, + "waxing_crescent_moon": { + "unicode": "1F312", + "unicode_alternates": [], + "name": "waxing crescent moon symbol", + "shortname": ":waxing_crescent_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature", "moon", "waxing", "sky", "night", "cheese", "phase"], + "moji": "🌒" + }, + "waxing_gibbous_moon": { + "unicode": "1F314", + "unicode_alternates": [], + "name": "waxing gibbous moon symbol", + "shortname": ":waxing_gibbous_moon:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["nature"], + "moji": "🌔" + }, + "wc": { + "unicode": "1F6BE", + "unicode_alternates": [], + "name": "water closet", + "shortname": ":wc:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "restroom", "toilet", "water", "closet", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"], + "moji": "🚾" + }, + "weary": { + "unicode": "1F629", + "unicode_alternates": [], + "name": "weary face", + "shortname": ":weary:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "frustrated", "sad", "sleepy", "tired", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted"], + "moji": "😩" + }, + "wedding": { + "unicode": "1F492", + "unicode_alternates": [], + "name": "wedding", + "shortname": ":wedding:", + "category": "places", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "bride", "couple", "groom", "like", "love", "marriage"], + "moji": "💒" + }, + "whale": { + "unicode": "1F433", + "unicode_alternates": [], + "name": "spouting whale", + "shortname": ":whale:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "ocean", "sea"], + "moji": "ðŸ³" + }, + "whale2": { + "unicode": "1F40B", + "unicode_alternates": [], + "name": "whale", + "shortname": ":whale2:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature", "ocean", "sea", "whale", "blubber", "bloated", "fat", "large", "massive"], + "moji": "ðŸ‹" + }, + "wheelchair": { + "unicode": "267F", + "unicode_alternates": ["267F-FE0F"], + "name": "wheelchair symbol", + "shortname": ":wheelchair:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "disabled"], + "moji": "♿" + }, + "white_check_mark": { + "unicode": "2705", + "unicode_alternates": [], + "name": "white heavy check mark", + "shortname": ":white_check_mark:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["agree", "green-square", "ok"], + "moji": "✅" + }, + "white_circle": { + "unicode": "26AA", + "unicode_alternates": ["26AA-FE0F"], + "name": "medium white circle", + "shortname": ":white_circle:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "⚪" + }, + "white_flower": { + "unicode": "1F4AE", + "unicode_alternates": [], + "name": "white flower", + "shortname": ":white_flower:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["japanese", "white", "flower", "teacher", "school", "grade", "score", "brilliance", "intelligence", "homework", "student", "assignment", "praise"], + "moji": "💮" + }, + "white_large_square": { + "unicode": "2B1C", + "unicode_alternates": ["2B1C-FE0F"], + "name": "white large square", + "shortname": ":white_large_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "⬜" + }, + "white_medium_small_square": { + "unicode": "25FD", + "unicode_alternates": ["25FD-FE0F"], + "name": "white medium small square", + "shortname": ":white_medium_small_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "â—½" + }, + "white_medium_square": { + "unicode": "25FB", + "unicode_alternates": ["25FB-FE0F"], + "name": "white medium square", + "shortname": ":white_medium_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "â—»" + }, + "white_small_square": { + "unicode": "25AB", + "unicode_alternates": ["25AB-FE0F"], + "name": "white small square", + "shortname": ":white_small_square:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "â–«" + }, + "white_square_button": { + "unicode": "1F533", + "unicode_alternates": [], + "name": "white square button", + "shortname": ":white_square_button:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["shape"], + "moji": "🔳" + }, + "wind_blowing_face": { + "unicode": "1F32C", + "unicode_alternates": [], + "name": "wind blowing face", + "shortname": ":wind_blowing_face:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["mother", "nature"] + }, + "wind_chime": { + "unicode": "1F390", + "unicode_alternates": [], + "name": "wind chime", + "shortname": ":wind_chime:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["ding", "nature", "wind", "chime", "bell", "fÅ«rin", "instrument", "music", "spirits", "soothing", "protective", "spiritual", "sound"], + "moji": "ðŸŽ" + }, + "wine_glass": { + "unicode": "1F377", + "unicode_alternates": [], + "name": "wine glass", + "shortname": ":wine_glass:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["alcohol", "beverage", "booze", "bottle", "drink", "drunk", "fermented", "glass", "grapes", "tasting", "wine", "winery"], + "moji": "ðŸ·" + }, + "wink": { + "unicode": "1F609", + "unicode_alternates": [], + "name": "winking face", + "shortname": ":wink:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [";)", ";-)", "*-)", "*)", ";-]", ";]", ";D", ";^)"], + "keywords": ["face", "happy", "mischievous", "secret", "wink", "winking", "friendly", "joke"], + "moji": "😉" + }, + "wolf": { + "unicode": "1F43A", + "unicode_alternates": [], + "name": "wolf face", + "shortname": ":wolf:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["animal", "nature"], + "moji": "ðŸº" + }, + "woman": { + "unicode": "1F469", + "unicode_alternates": [], + "name": "woman", + "shortname": ":woman:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["female", "girls"], + "moji": "👩" + }, + "womans_clothes": { + "unicode": "1F45A", + "unicode_alternates": [], + "name": "womans clothes", + "shortname": ":womans_clothes:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["fashion", "woman", "clothing", "clothes", "blouse", "shirt", "wardrobe", "breasts", "cleavage", "shopping", "shop", "dressing", "dressed"], + "moji": "👚" + }, + "womans_hat": { + "unicode": "1F452", + "unicode_alternates": [], + "name": "womans hat", + "shortname": ":womans_hat:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["accessories", "fashion", "female"], + "moji": "👒" + }, + "womens": { + "unicode": "1F6BA", + "unicode_alternates": [], + "name": "womens symbol", + "shortname": ":womens:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["purple-square", "woman", "bathroom", "restroom", "sign", "girl", "female", "avatar"], + "moji": "🚺" + }, + "worried": { + "unicode": "1F61F", + "unicode_alternates": [], + "name": "worried face", + "shortname": ":worried:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["concern", "face", "nervous", "worried", "anxious", "distressed", "nervous", "tense"], + "moji": "😟" + }, + "wrench": { + "unicode": "1F527", + "unicode_alternates": [], + "name": "wrench", + "shortname": ":wrench:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["diy", "ikea", "tools"], + "moji": "🔧" + }, + "writing_hand": { + "unicode": "1F58E", + "unicode_alternates": [], + "name": "left writing hand", + "shortname": ":writing_hand:", + "category": "people", + "aliases": [":left_writing_hand:"], + "aliases_ascii": [], + "keywords": ["write", "sign", "signature", "draw"] + }, + "x": { + "unicode": "274C", + "unicode_alternates": [], + "name": "cross mark", + "shortname": ":x:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["delete", "no", "remove"], + "moji": "âŒ" + }, + "yellow_heart": { + "unicode": "1F49B", + "unicode_alternates": [], + "name": "yellow heart", + "shortname": ":yellow_heart:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["affection", "like", "love", "valentines", "yellow", "gold", "heart", "love", "friendship", "happy", "happiness", "trust", "compassionate", "respectful", "honest", "caring", "selfless"], + "moji": "💛" + }, + "yen": { + "unicode": "1F4B4", + "unicode_alternates": [], + "name": "banknote with yen sign", + "shortname": ":yen:", + "category": "objects", + "aliases": [], + "aliases_ascii": [], + "keywords": ["currency", "dollar", "japanese", "money", "yen", "japan", "japanese", "banknote", "money", "currency", "paper", "cash", "bill"], + "moji": "💴" + }, + "yum": { + "unicode": "1F60B", + "unicode_alternates": [], + "name": "face savouring delicious food", + "shortname": ":yum:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["face", "happy", "joy", "smile", "tongue", "delicious", "savoring", "food", "eat", "yummy", "yum", "tasty", "savory"], + "moji": "😋" + }, + "zap": { + "unicode": "26A1", + "unicode_alternates": ["26A1-FE0F"], + "name": "high voltage sign", + "shortname": ":zap:", + "category": "nature", + "aliases": [], + "aliases_ascii": [], + "keywords": ["lightning bolt", "thunder", "weather"], + "moji": "âš¡" + }, + "zero": { + "moji": "0ï¸âƒ£", + "unicode": "0030-20E3", + "unicode_alternates": ["0030-FE0F-20E3"], + "name": "digit zero", + "shortname": ":zero:", + "category": "other", + "aliases": [], + "aliases_ascii": [], + "keywords": ["blue-square", "null", "numbers"] + }, + "zzz": { + "unicode": "1F4A4", + "unicode_alternates": [], + "name": "sleeping symbol", + "shortname": ":zzz:", + "category": "emoticons", + "aliases": [], + "aliases_ascii": [], + "keywords": ["sleepy", "tired"], + "moji": "💤" + } +} diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b1cd80bdf65a246e7f9e6ea543c94d9d675f8c2e..26e7c956e8f79f2bc23ba4650c9e64693d1e75b0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -67,9 +67,10 @@ module API expose :shared_runners_enabled expose :creator_id expose :namespace - expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? } expose :avatar_url expose :star_count, :forks_count + expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } end class ProjectMember < UserBasic @@ -165,7 +166,6 @@ module API class MergeRequest < ProjectEntity expose :target_branch, :source_branch - # deprecated, always returns 0 expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id diff --git a/lib/api/files.rb b/lib/api/files.rb index a7a768f8895ded69e5bfeba3c9e38819af79e6d6..8ad2c1883c77e7b91ee86026f5217264585eb235 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -7,7 +7,7 @@ module API def commit_params(attrs) { file_path: attrs[:file_path], - current_branch: attrs[:branch_name], + source_branch: attrs[:branch_name], target_branch: attrs[:branch_name], commit_message: attrs[:commit_message], file_content: attrs[:content], diff --git a/lib/api/projects.rb b/lib/api/projects.rb index bdf4b77596e41f313d5ea0e41cb12d881f9b7fdb..a9e0960872a08cb1b455a448b8458364acd95e8d 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -25,7 +25,7 @@ module API @projects = current_user.authorized_projects @projects = filter_projects(@projects) @projects = paginate @projects - present @projects, with: Entities::Project + present @projects, with: Entities::ProjectWithAccess, user: current_user end # Get an owned projects list for authenticated user @@ -36,6 +36,17 @@ module API @projects = current_user.owned_projects @projects = filter_projects(@projects) @projects = paginate @projects + present @projects, with: Entities::ProjectWithAccess, user: current_user + end + + # Gets starred project for the authenticated user + # + # Example Request: + # GET /projects/starred + get '/starred' do + @projects = current_user.starred_projects + @projects = filter_projects(@projects) + @projects = paginate @projects present @projects, with: Entities::Project end @@ -48,7 +59,7 @@ module API @projects = Project.all @projects = filter_projects(@projects) @projects = paginate @projects - present @projects, with: Entities::Project + present @projects, with: Entities::ProjectWithAccess, user: current_user end # Get a single project diff --git a/lib/api/users.rb b/lib/api/users.rb index a98d668e02d0db0c36719212d83bae75fcbb7c5a..3400f0713ef11c010e93680f41dd3a6e2e8796fb 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -8,11 +8,17 @@ module API # # Example Request: # GET /users + # GET /users?search=Admin + # GET /users?username=root get do - @users = User.all - @users = @users.active if params[:active].present? - @users = @users.search(params[:search]) if params[:search].present? - @users = paginate @users + if params[:username].present? + @users = User.where(username: params[:username]) + else + @users = User.all + @users = @users.active if params[:active].present? + @users = @users.search(params[:search]) if params[:search].present? + @users = paginate @users + end if current_user.is_admin? present @users, with: Entities::UserFull diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 4d99164bc3319e8fc1dc241627d8735da93d1115..783fcfb61ad0363326eac5abff1970e1de318814 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -1,47 +1,51 @@ class AwardEmoji - EMOJI_LIST = [ - "+1", "-1", "100", "blush", "heart", "smile", "rage", - "beers", "disappointed", "ok_hand", - "helicopter", "shit", "airplane", "alarm_clock", - "ambulance", "anguished", "two_hearts", "wink" - ] - - ALIASES = { - pout: "rage", - satisfied: "laughing", - hankey: "shit", - poop: "shit", - collision: "boom", - thumbsup: "+1", - thumbsdown: "-1", - punch: "facepunch", - raised_hand: "hand", - running: "runner", - ng_woman: "no_good", - shoe: "mans_shoe", - tshirt: "shirt", - honeybee: "bee", - flipper: "dolphin", - paw_prints: "feet", - waxing_gibbous_moon: "moon", - telephone: "phone", - knife: "hocho", - envelope: "email", - pencil: "memo", - open_book: "book", - sailboat: "boat", - red_car: "car", - lantern: "izakaya_lantern", - uk: "gb", - heavy_exclamation_mark: "exclamation", - squirrel: "shipit" + CATEGORIES = { + other: "Other", + objects: "Objects", + places: "Places", + travel_places: "Travel", + emoticons: "Emoticons", + objects_symbols: "Symbols", + nature: "Nature", + celebration: "Celebration", + people: "People", + activity: "Activity", + flags: "Flags", + food_drink: "Food" }.with_indifferent_access - def self.path_to_emoji_image(name) - "emoji/#{Emoji.emoji_filename(name)}.png" + def self.normilize_emoji_name(name) + aliases[name] || name end - def self.normilize_emoji_name(name) - ALIASES[name] || name + def self.emoji_by_category + unless @emoji_by_category + @emoji_by_category = {} + + emojis.each do |emoji_name, data| + data["name"] = emoji_name + + @emoji_by_category[data["category"]] ||= [] + @emoji_by_category[data["category"]] << data + end + + @emoji_by_category = @emoji_by_category.sort.to_h + end + + @emoji_by_category + end + + def self.emojis + @emojis ||= begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) + JSON.parse(File.read(json_path)) + end + end + + def self.aliases + @aliases ||= begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) + JSON.parse(File.read(json_path)) + end end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index bdaa4721b4bcdc863f07f1dab60f3bc4bd22f292..63ad8910c0ff31b774051f4e66a4ea145a041a21 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -98,7 +98,7 @@ module Banzai project = project_from_ref(project_ref) if project && object = find_object(project, id) - title = escape_once(object_link_title(object)) + title = object_link_title(object) klass = reference_class(object_sym) data = data_attribute( @@ -110,17 +110,11 @@ module Banzai url = matches[:url] if matches.names.include?("url") url ||= url_for_object(object, project) - text = link_text - unless text - text = object.reference_link_text(context[:project]) - - extras = object_link_text_extras(object, matches) - text += " (#{extras.join(", ")})" if extras.any? - end + text = link_text || object_link_text(object, matches) %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{text}</a>) + title="#{escape_once(title)}" + class="#{klass}">#{escape_once(text)}</a>) else match end @@ -140,6 +134,15 @@ module Banzai def object_link_title(object) "#{object_class.name.titleize}: #{object.title}" end + + def object_link_text(object, matches) + text = object.reference_link_text(context[:project]) + + extras = object_link_text_extras(object, matches) + text += " (#{extras.join(", ")})" if extras.any? + + text + end end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index f5737a7ac193afb9d40d96e7838371adefd72154..6136e73c096ab7df26c281a903840b64695e0803 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -23,6 +23,18 @@ module Banzai end end + def self.referenced_by(node) + project = Project.find(node.attr("data-project")) rescue nil + return unless project + + id = node.attr("data-external-issue") + external_issue = ExternalIssue.new(id, project) + + return unless external_issue + + { external_issue: external_issue } + end + def call # Early return if the project isn't using an external tracker return doc if project.nil? || project.default_issues_tracker? @@ -46,18 +58,20 @@ module Banzai def issue_link_filter(text, link_text: nil) project = context[:project] - self.class.references_in(text) do |match, issue| - url = url_for_issue(issue, project, only_path: context[:only_path]) + self.class.references_in(text) do |match, id| + ExternalIssue.new(id, project) + + url = url_for_issue(id, project, only_path: context[:only_path]) - title = escape_once("Issue in #{project.external_issue_tracker.title}") + title = "Issue in #{project.external_issue_tracker.title}" klass = reference_class(:issue) - data = data_attribute(project: project.id) + data = data_attribute(project: project.id, external_issue: id) text = link_text || match %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{text}</a>) + title="#{escape_once(title)}" + class="#{klass}">#{escape_once(text)}</a>) end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 07bac2dd7fd4897fcee8ea09567f803ceff03c58..a3a7a23c1e6ae272f950dcb76154c837d830c3e0 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -60,7 +60,7 @@ module Banzai text = link_text || render_colored_label(label) %(<a href="#{url}" #{data} - class="#{klass}">#{text}</a>) + class="#{klass}">#{escape_once(text)}</a>) else match end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index 0072bab1f99e2c3b06561a4823f64bcb94846f61..d09cf41df394d478a04f125da729b2f13e03f235 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -6,7 +6,7 @@ module Banzai class MarkdownFilter < HTML::Pipeline::TextFilter def initialize(text, context = nil, result = nil) super text, context, result - @text = @text.gsub "\r", '' + @text = @text.delete "\r" end def call diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index 89e7a79789a7990feecab47884a224c31427930a..f01a32b5ae5e6055f69183e5005f7b08b8062fc7 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -11,7 +11,7 @@ module Banzai class RedactorFilter < HTML::Pipeline::Filter def call doc.css('a.gfm').each do |node| - unless user_can_reference?(node) + unless user_can_see_reference?(node) # The reference should be replaced by the original text, # which is not always the same as the rendered text. text = node.attr('data-original') || node.text @@ -24,12 +24,12 @@ module Banzai private - def user_can_reference?(node) + def user_can_see_reference?(node) if node.has_attribute?('data-reference-filter') reference_type = node.attr('data-reference-filter') reference_filter = Banzai::Filter.const_get(reference_type) - reference_filter.user_can_reference?(current_user, node, context) + reference_filter.user_can_see_reference?(current_user, node, context) else true end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 33457a3f361837e6daa228047fa48080ab80d09e..8ca05ace88cc55ea168935ed7d245b8ba61a7b40 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -12,7 +12,7 @@ module Banzai # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. class ReferenceFilter < HTML::Pipeline::Filter - def self.user_can_reference?(user, node, context) + def self.user_can_see_reference?(user, node, context) if node.has_attribute?('data-project') project_id = node.attr('data-project').to_i return true if project_id == context[:project].try(:id) @@ -24,6 +24,10 @@ module Banzai end end + def self.user_can_reference?(user, node, context) + true + end + def self.referenced_by(node) raise NotImplementedError, "#{self} does not implement #{__method__}" end @@ -44,11 +48,11 @@ module Banzai # Returns a String def data_attribute(attributes = {}) attributes[:reference_filter] = self.class.name.demodulize - attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") + attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") end def escape_once(html) - ERB::Util.html_escape_once(html) + html.html_safe? ? html : ERB::Util.html_escape_once(html) end def ignore_parents diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb index 855f238ac1ebf4d79c7f8c396aab45911b730846..12412ff7ea9267bc32244758d76edef439724669 100644 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ b/lib/banzai/filter/reference_gatherer_filter.rb @@ -35,7 +35,9 @@ module Banzai return if context[:reference_filter] && reference_filter != context[:reference_filter] - return unless reference_filter.user_can_reference?(current_user, node, context) + return if author && !reference_filter.user_can_reference?(author, node, context) + + return unless reference_filter.user_can_see_reference?(current_user, node, context) references = reference_filter.referenced_by(node) return unless references @@ -57,6 +59,10 @@ module Banzai def current_user context[:current_user] end + + def author + context[:author] + end end end end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 92d130074dcd4d93c8ac382c0e1366c9174562a0..9b3e67206d5539c49f00d993cb9bdfa9c3ba5cd2 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -31,7 +31,7 @@ module Banzai id = text.downcase id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation - id.gsub!(' ', '-') # replace spaces with dash + id.tr!(' ', '-') # replace spaces with dash id.squeeze!('-') # replace multiple dashes with one uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 67c24faf991243f4aa167fbde94ff17a9ccbc129..964ab60f614b9f3a75ab1039e71a3cd2abf733f9 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai end end - def self.user_can_reference?(user, node, context) + def self.user_can_see_reference?(user, node, context) if node.has_attribute?('data-group') group = Group.find(node.attr('data-group')) rescue nil Ability.abilities.allowed?(user, :read_group, group) @@ -48,6 +48,18 @@ module Banzai end end + def self.user_can_reference?(user, node, context) + # Only team members can reference `@all` + if node.has_attribute?('data-project') + project = Project.find(node.attr('data-project')) rescue nil + return false unless project + + user && project.team.member?(user) + else + super + end + end + def call replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) @@ -122,7 +134,7 @@ module Banzai end def link_tag(url, data, text) - %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) + %(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>) end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 891c0fd7749aac0f984d41ecfe572054596dcea9..115ae91452486b304779a1f1d342ab16124f99e2 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -1,5 +1,7 @@ module Banzai module Renderer + CACHE_ENABLED = false + # Convert a Markdown String into an HTML-safe String of HTML # # Note that while the returned HTML will have been sanitized of dangerous @@ -18,7 +20,7 @@ module Banzai cache_key = context.delete(:cache_key) cache_key = full_cache_key(cache_key, context[:pipeline]) - if cache_key + if cache_key && CACHE_ENABLED Rails.cache.fetch(cache_key) do cacheless_render(text, context) end diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 443563c2e4a4c8b1cb48af76ff093c891ad25fd0..1c91204e98ca1c9331f0c597d4e8bb5933b3c7cd 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -19,7 +19,7 @@ module Ci end def runner_registration_token_valid? - params[:token] == current_application_settings.ensure_runners_registration_token + params[:token] == current_application_settings.runners_registration_token end def update_runner_last_contact diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 87ac30b5ffef959e7baa2a60f8e28d41310099ed..459e3d6bcdbf1e6c6235ae5a59d4128dce3011a1 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -2,7 +2,7 @@ module Gitlab class Shell class Error < StandardError; end - class KeyAdder < Struct.new(:io) + KeyAdder = Struct.new(:io) do def add_key(id, key) key.gsub!(/[[:space:]]+/, ' ').strip! io.puts("#{id}\t#{key}") diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 35e34d033e0ccc8a31b501f8412b5a58b0231917..03aac1a025a8d14b244a62b15cec763e56d16fb6 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo["name"], path: repo["slug"], description: repo["description"], diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 46a4ef0e31febfd300cbc35bc5abb88291e59f27..7a86c09158ed4fa8712945284344225dea21ec08 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -38,7 +38,9 @@ module Gitlab true end - use_db && ActiveRecord::Base.connection.active? && ActiveRecord::Base.connection.table_exists?('application_settings') + use_db && ActiveRecord::Base.connection.active? && + !ActiveRecord::Migrator.needs_migration? && + ActiveRecord::Base.connection.table_exists?('application_settings') end end end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 142058aa69d46e0b1429c5e0b0b8f18ae09cfb16..79061cd014181d0375b622431ae41c5379f493c6 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -46,11 +46,11 @@ module Gitlab end def added_lines - diff_lines.select(&:added?).size + diff_lines.count(&:added?) end def removed_lines - diff_lines.select(&:removed?).size + diff_lines.count(&:removed?) end end end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 496256700b87e6059ef37a943f8f8e595f553b79..403ebeec47417d678a0a617fa6697ec89ba0953a 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -199,7 +199,7 @@ module Gitlab s = s.gsub(/^#/, "\\#") s = s.gsub(/^-/, "\\-") s = s.gsub("`", "\\~") - s = s.gsub("\r", "") + s = s.delete("\r") s = s.gsub("\n", " \n") s end diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 8b1b6f48ed500573377c4e91ed59405f12b28c0a..e0163499e3094066b386167d9527eaa60f6158fa 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo.safe_name, path: repo.path, namespace: namespace, diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 0c350d7c675e1977077944a7c1eff2f5a355c93b..f065cc5e9e9962c70407370403881452dab5c795 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -20,6 +20,10 @@ module Gitlab def blank_ref?(ref) ref == BLANK_SHA end + + def version + Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first) + end end end end diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index d9452de6a5093b99990a79f44fb29ce1986d6098..7baaadb813c60ab555f2f4c993bc6c5b5f24c2dc 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo["name"], path: repo["path"], description: repo["description"], diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb index cc9a91c91f4538faa2158d8b11234874863b128e..8e22aa9286ddf4d2a332f2daca87a6ab5f2eee9d 100644 --- a/lib/gitlab/gitorious_import/project_creator.rb +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -10,7 +10,8 @@ module Gitlab end def execute - ::Projects::CreateService.new(current_user, + ::Projects::CreateService.new( + current_user, name: repo.name, path: repo.path, description: repo.description, diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 87fee28dc010adfcae54c903c427a67d8634b54a..62da327931faff96264b4152325c321bc7f16281 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -171,8 +171,6 @@ module Gitlab when /\AMilestone:/ "#fee3ff" - when *@closed_statuses.map { |s| nice_status_name(s) } - "#cfcfcf" when "Status: New" "#428bca" when "Status: Accepted" @@ -199,6 +197,8 @@ module Gitlab "#8e44ad" when "Type: Other" "#7f8c8d" + when *@closed_statuses.map { |s| nice_status_name(s) } + "#cfcfcf" else "#e2e2e2" end @@ -227,7 +227,7 @@ module Gitlab s = s.gsub("`", "\\`") # Carriage returns make me sad - s = s.gsub("\r", "") + s = s.delete("\r") # Markdown ignores single newlines, but we need them as <br />. s = s.gsub("\n", " \n") diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 1cb7d16aeb3f0021ea7528f29b5be797e3b2caa1..87821c2346094f94869b8144a2a6412c739618af 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,8 @@ module Gitlab end def execute - project = ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo.name, path: repo.name, description: repo.summary, diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 4be99dd88c29dd87ba829973da60aeca464dbf2d..aef08c97d1d7edf18a23b02f82a0eedcb5af40e1 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -14,7 +14,7 @@ module Gitlab # LDAP distinguished name is case-insensitive identity = ::Identity. where(provider: provider). - where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last + iwhere(extern_uid: uid).last identity && identity.user end end @@ -31,7 +31,7 @@ module Gitlab def find_by_uid_and_provider self.class.find_by_uid_and_provider( - auth_hash.uid.downcase, auth_hash.provider) + auth_hash.uid, auth_hash.provider) end def find_by_email @@ -47,7 +47,7 @@ module Gitlab # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved. identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider } identity ||= gl_user.identities.build(provider: auth_hash.provider) - + # For a new user set extern_uid to the LDAP DN # For an existing user with matching email but changed DN, update the DN. # For an existing user with no change in DN, this line changes nothing. diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d266ccfe9e64252996ff290ed1d42ab4c6a09c2 --- /dev/null +++ b/lib/gitlab/metrics.rb @@ -0,0 +1,104 @@ +module Gitlab + module Metrics + extend Gitlab::CurrentSettings + + RAILS_ROOT = Rails.root.to_s + METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s + PATH_REGEX = /^#{RAILS_ROOT}\/?/ + + def self.pool_size + current_application_settings[:metrics_pool_size] || 16 + end + + def self.timeout + current_application_settings[:metrics_timeout] || 10 + end + + def self.enabled? + current_application_settings[:metrics_enabled] || false + end + + def self.mri? + RUBY_ENGINE == 'ruby' + end + + def self.method_call_threshold + # This is memoized since this method is called for every instrumented + # method. Loading data from an external cache on every method call slows + # things down too much. + @method_call_threshold ||= + (current_application_settings[:metrics_method_call_threshold] || 10) + end + + def self.pool + @pool + end + + def self.hostname + @hostname + end + + # Returns a relative path and line number based on the last application call + # frame. + def self.last_relative_application_frame + frame = caller_locations.find do |l| + l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT) + end + + if frame + return frame.path.sub(PATH_REGEX, ''), frame.lineno + else + return nil, nil + end + end + + def self.submit_metrics(metrics) + prepared = prepare_metrics(metrics) + + pool.with do |connection| + prepared.each do |metric| + begin + connection.write_points([metric]) + rescue StandardError + end + end + end + end + + def self.prepare_metrics(metrics) + metrics.map do |hash| + new_hash = hash.symbolize_keys + + new_hash[:tags].each do |key, value| + if value.blank? + new_hash[:tags].delete(key) + else + new_hash[:tags][key] = escape_value(value) + end + end + + new_hash + end + end + + def self.escape_value(value) + value.to_s.gsub('=', '\\=') + end + + @hostname = Socket.gethostname + + # When enabled this should be set before being used as the usual pattern + # "@foo ||= bar" is _not_ thread-safe. + if enabled? + @pool = ConnectionPool.new(size: pool_size, timeout: timeout) do + host = current_application_settings[:metrics_host] + user = current_application_settings[:metrics_username] + pw = current_application_settings[:metrics_password] + port = current_application_settings[:metrics_port] + + InfluxDB::Client. + new(udp: { host: host, port: port }, username: user, password: pw) + end + end + end +end diff --git a/lib/gitlab/metrics/delta.rb b/lib/gitlab/metrics/delta.rb new file mode 100644 index 0000000000000000000000000000000000000000..bcf28eed84d897c7f08cfd6691cbc9c4c8fadb68 --- /dev/null +++ b/lib/gitlab/metrics/delta.rb @@ -0,0 +1,32 @@ +module Gitlab + module Metrics + # Class for calculating the difference between two numeric values. + # + # Every call to `compared_with` updates the internal value. This makes it + # possible to use a single Delta instance to calculate the delta over time + # of an ever increasing number. + # + # Example usage: + # + # delta = Delta.new(0) + # + # delta.compared_with(10) # => 10 + # delta.compared_with(15) # => 5 + # delta.compared_with(20) # => 5 + class Delta + def initialize(value = 0) + @value = value + end + + # new_value - The value to compare with as a Numeric. + # + # Returns a new Numeric (depending on the type of `new_value`). + def compared_with(new_value) + delta = new_value - @value + @value = new_value + + delta + end + end + end +end diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb new file mode 100644 index 0000000000000000000000000000000000000000..06fc2f259483d713e8a6340496980f27b5dfbf23 --- /dev/null +++ b/lib/gitlab/metrics/instrumentation.rb @@ -0,0 +1,146 @@ +module Gitlab + module Metrics + # Module for instrumenting methods. + # + # This module allows instrumenting of methods without having to actually + # alter the target code (e.g. by including modules). + # + # Example usage: + # + # Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login) + module Instrumentation + SERIES = 'method_calls' + + def self.configure + yield self + end + + # Instruments a class method. + # + # mod - The module to instrument as a Module/Class. + # name - The name of the method to instrument. + def self.instrument_method(mod, name) + instrument(:class, mod, name) + end + + # Instruments an instance method. + # + # mod - The module to instrument as a Module/Class. + # name - The name of the method to instrument. + def self.instrument_instance_method(mod, name) + instrument(:instance, mod, name) + end + + # Recursively instruments all subclasses of the given root module. + # + # This can be used to for example instrument all ActiveRecord models (as + # these all inherit from ActiveRecord::Base). + # + # This method can optionally take a block to pass to `instrument_methods` + # and `instrument_instance_methods`. + # + # root - The root module for which to instrument subclasses. The root + # module itself is not instrumented. + def self.instrument_class_hierarchy(root, &block) + visit = root.subclasses + + until visit.empty? + klass = visit.pop + + instrument_methods(klass, &block) + instrument_instance_methods(klass, &block) + + klass.subclasses.each { |c| visit << c } + end + end + + # Instruments all public methods of a module. + # + # This method optionally takes a block that can be used to determine if a + # method should be instrumented or not. The block is passed the receiving + # module and an UnboundMethod. If the block returns a non truthy value the + # method is not instrumented. + # + # mod - The module to instrument. + def self.instrument_methods(mod) + mod.public_methods(false).each do |name| + method = mod.method(name) + + if method.owner == mod.singleton_class + if !block_given? || block_given? && yield(mod, method) + instrument_method(mod, name) + end + end + end + end + + # Instruments all public instance methods of a module. + # + # See `instrument_methods` for more information. + # + # mod - The module to instrument. + def self.instrument_instance_methods(mod) + mod.public_instance_methods(false).each do |name| + method = mod.instance_method(name) + + if method.owner == mod + if !block_given? || block_given? && yield(mod, method) + instrument_instance_method(mod, name) + end + end + end + end + + # Instruments a method. + # + # type - The type (:class or :instance) of method to instrument. + # mod - The module containing the method. + # name - The name of the method to instrument. + def self.instrument(type, mod, name) + return unless Metrics.enabled? + + name = name.to_sym + alias_name = :"_original_#{name}" + target = type == :instance ? mod : mod.singleton_class + + if type == :instance + target = mod + label = "#{mod.name}##{name}" + else + target = mod.singleton_class + label = "#{mod.name}.#{name}" + end + + target.class_eval <<-EOF, __FILE__, __LINE__ + 1 + alias_method #{alias_name.inspect}, #{name.inspect} + + def #{name}(*args, &block) + trans = Gitlab::Metrics::Instrumentation.transaction + + if trans + start = Time.now + retval = __send__(#{alias_name.inspect}, *args, &block) + duration = (Time.now - start) * 1000.0 + + if duration >= Gitlab::Metrics.method_call_threshold + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, + { duration: duration }, + method: #{label.inspect}) + end + + retval + else + __send__(#{alias_name.inspect}, *args, &block) + end + end + EOF + end + + # Small layer of indirection to make it easier to stub out the current + # transaction. + def self.transaction + Transaction.current + end + end + end +end diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb new file mode 100644 index 0000000000000000000000000000000000000000..753008df99af03142b2b1954afc0ec24601e0731 --- /dev/null +++ b/lib/gitlab/metrics/metric.rb @@ -0,0 +1,31 @@ +module Gitlab + module Metrics + # Class for storing details of a single metric (label, value, etc). + class Metric + attr_reader :series, :values, :tags, :created_at + + # series - The name of the series (as a String) to store the metric in. + # values - A Hash containing the values to store. + # tags - A Hash containing extra tags to add to the metrics. + def initialize(series, values, tags = {}) + @values = values + @series = series + @tags = tags + @created_at = Time.now.utc + end + + # Returns a Hash in a format that can be directly written to InfluxDB. + def to_hash + { + series: @series, + tags: @tags.merge( + hostname: Metrics.hostname, + process_type: Sidekiq.server? ? 'sidekiq' : 'rails' + ), + values: @values, + timestamp: @created_at.to_i * 1_000_000_000 + } + end + end + end +end diff --git a/lib/gitlab/metrics/obfuscated_sql.rb b/lib/gitlab/metrics/obfuscated_sql.rb new file mode 100644 index 0000000000000000000000000000000000000000..fe97d7a0534e1e160d74051ec4ee30fe7631a3fc --- /dev/null +++ b/lib/gitlab/metrics/obfuscated_sql.rb @@ -0,0 +1,47 @@ +module Gitlab + module Metrics + # Class for producing SQL queries with sensitive data stripped out. + class ObfuscatedSQL + REPLACEMENT = / + \d+(\.\d+)? # integers, floats + | '.+?' # single quoted strings + | \/.+?(?<!\\)\/ # regexps (including escaped slashes) + /x + + MYSQL_REPLACEMENTS = / + ".+?" # double quoted strings + /x + + # Regex to replace consecutive placeholders with a single one indicating + # the length. This can be useful when a "IN" statement uses thousands of + # IDs (storing this would just be a waste of space). + CONSECUTIVE = /(\?(\s*,\s*)?){2,}/ + + # sql - The raw SQL query as a String. + def initialize(sql) + @sql = sql + end + + # Returns a new, obfuscated SQL query. + def to_s + regex = REPLACEMENT + + if Gitlab::Database.mysql? + regex = Regexp.union(regex, MYSQL_REPLACEMENTS) + end + + sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match| + "#{match.count(',') + 1} values" + end + + # InfluxDB escapes double quotes upon output, so lets get rid of them + # whenever we can. + if Gitlab::Database.postgresql? + sql = sql.delete('"') + end + + sql.tr("\n", ' ') + end + end + end +end diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb new file mode 100644 index 0000000000000000000000000000000000000000..5c0587c4c51396d331a67681650b60b50dfa5df4 --- /dev/null +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -0,0 +1,49 @@ +module Gitlab + module Metrics + # Rack middleware for tracking Rails requests. + class RackMiddleware + CONTROLLER_KEY = 'action_controller.instance' + + def initialize(app) + @app = app + end + + # env - A Hash containing Rack environment details. + def call(env) + trans = transaction_from_env(env) + retval = nil + + begin + retval = trans.run { @app.call(env) } + + # Even in the event of an error we want to submit any metrics we + # might've gathered up to this point. + ensure + if env[CONTROLLER_KEY] + tag_controller(trans, env) + end + + trans.finish + end + + retval + end + + def transaction_from_env(env) + trans = Transaction.new + + trans.add_tag(:request_method, env['REQUEST_METHOD']) + trans.add_tag(:request_uri, env['REQUEST_URI']) + + trans + end + + def tag_controller(trans, env) + controller = env[CONTROLLER_KEY] + label = "#{controller.class.name}##{controller.action_name}" + + trans.add_tag(:action, label) + end + end + end +end diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb new file mode 100644 index 0000000000000000000000000000000000000000..998578e1c0a704e9f4c120474dfe2baa2e95604b --- /dev/null +++ b/lib/gitlab/metrics/sampler.rb @@ -0,0 +1,98 @@ +module Gitlab + module Metrics + # Class that sends certain metrics to InfluxDB at a specific interval. + # + # This class is used to gather statistics that can't be directly associated + # with a transaction such as system memory usage, garbage collection + # statistics, etc. + class Sampler + # interval - The sampling interval in seconds. + def initialize(interval = 15) + @interval = interval + @metrics = [] + + @last_minor_gc = Delta.new(GC.stat[:minor_gc_count]) + @last_major_gc = Delta.new(GC.stat[:major_gc_count]) + + if Gitlab::Metrics.mri? + require 'allocations' + + Allocations.start + end + end + + def start + Thread.new do + Thread.current.abort_on_exception = true + + loop do + sleep(@interval) + + sample + end + end + end + + def sample + sample_memory_usage + sample_file_descriptors + sample_objects + sample_gc + + flush + ensure + GC::Profiler.clear + @metrics.clear + end + + def flush + Metrics.submit_metrics(@metrics.map(&:to_hash)) + end + + def sample_memory_usage + @metrics << Metric.new('memory_usage', value: System.memory_usage) + end + + def sample_file_descriptors + @metrics << Metric. + new('file_descriptors', value: System.file_descriptor_count) + end + + if Metrics.mri? + def sample_objects + sample = Allocations.to_hash + counts = sample.each_with_object({}) do |(klass, count), hash| + hash[klass.name] = count + end + + # Symbols aren't allocated so we'll need to add those manually. + counts['Symbol'] = Symbol.all_symbols.length + + counts.each do |name, count| + @metrics << Metric.new('object_counts', { count: count }, type: name) + end + end + else + def sample_objects + end + end + + def sample_gc + time = GC::Profiler.total_time * 1000.0 + stats = GC.stat.merge(total_time: time) + + # We want the difference of GC runs compared to the last sample, not the + # total amount since the process started. + stats[:minor_gc_count] = + @last_minor_gc.compared_with(stats[:minor_gc_count]) + + stats[:major_gc_count] = + @last_major_gc.compared_with(stats[:major_gc_count]) + + stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count] + + @metrics << Metric.new('gc_statistics', stats) + end + end + end +end diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad441decfa251670545e86ba10cb0e074c1fc9d2 --- /dev/null +++ b/lib/gitlab/metrics/sidekiq_middleware.rb @@ -0,0 +1,23 @@ +module Gitlab + module Metrics + # Sidekiq middleware for tracking jobs. + # + # This middleware is intended to be used as a server-side middleware. + class SidekiqMiddleware + def call(worker, message, queue) + trans = Transaction.new + + begin + trans.run { yield } + ensure + tag_worker(trans, worker) + trans.finish + end + end + + def tag_worker(trans, worker) + trans.add_tag(:action, "#{worker.class.name}#perform") + end + end + end +end diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e0dcf99d926178ccfd52ee176f31a204646edbf --- /dev/null +++ b/lib/gitlab/metrics/subscribers/action_view.rb @@ -0,0 +1,53 @@ +module Gitlab + module Metrics + module Subscribers + # Class for tracking the rendering timings of views. + class ActionView < ActiveSupport::Subscriber + attach_to :action_view + + SERIES = 'views' + + def render_template(event) + track(event) if current_transaction + end + + alias_method :render_view, :render_template + + private + + def track(event) + values = values_for(event) + tags = tags_for(event) + + current_transaction.add_metric(SERIES, values, tags) + end + + def relative_path(path) + path.gsub(/^#{Rails.root.to_s}\/?/, '') + end + + def values_for(event) + { duration: event.duration } + end + + def tags_for(event) + path = relative_path(event.payload[:identifier]) + tags = { view: path } + + file, line = Metrics.last_relative_application_frame + + if file and line + tags[:file] = file + tags[:line] = line + end + + tags + end + + def current_transaction + Transaction.current + end + end + end + end +end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb new file mode 100644 index 0000000000000000000000000000000000000000..d947c128ce22249a882d0816878bcb7b8bca23b5 --- /dev/null +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -0,0 +1,48 @@ +module Gitlab + module Metrics + module Subscribers + # Class for tracking raw SQL queries. + # + # Queries are obfuscated before being logged to ensure no private data is + # exposed via InfluxDB/Grafana. + class ActiveRecord < ActiveSupport::Subscriber + attach_to :active_record + + SERIES = 'sql_queries' + + def sql(event) + return unless current_transaction + + values = values_for(event) + tags = tags_for(event) + + current_transaction.add_metric(SERIES, values, tags) + end + + private + + def values_for(event) + { duration: event.duration } + end + + def tags_for(event) + sql = ObfuscatedSQL.new(event.payload[:sql]).to_s + tags = { sql: sql } + + file, line = Metrics.last_relative_application_frame + + if file and line + tags[:file] = file + tags[:line] = line + end + + tags + end + + def current_transaction + Transaction.current + end + end + end + end +end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb new file mode 100644 index 0000000000000000000000000000000000000000..83371265278f6da3b93d1bb34bd0186771e6cc7f --- /dev/null +++ b/lib/gitlab/metrics/system.rb @@ -0,0 +1,35 @@ +module Gitlab + module Metrics + # Module for gathering system/process statistics such as the memory usage. + # + # This module relies on the /proc filesystem being available. If /proc is + # not available the methods of this module will be stubbed. + module System + if File.exist?('/proc') + # Returns the current process' memory usage in bytes. + def self.memory_usage + mem = 0 + match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/) + + if match and match[1] + mem = match[1].to_f * 1024 + end + + mem + end + + def self.file_descriptor_count + Dir.glob('/proc/self/fd/*').length + end + else + def self.memory_usage + 0.0 + end + + def self.file_descriptor_count + 0 + end + end + end + end +end diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb new file mode 100644 index 0000000000000000000000000000000000000000..a61dbd989e782a064cc7f3c0cd7f8074de873579 --- /dev/null +++ b/lib/gitlab/metrics/transaction.rb @@ -0,0 +1,66 @@ +module Gitlab + module Metrics + # Class for storing metrics information of a single transaction. + class Transaction + THREAD_KEY = :_gitlab_metrics_transaction + + SERIES = 'transactions' + + attr_reader :uuid, :tags + + def self.current + Thread.current[THREAD_KEY] + end + + # name - The name of this transaction as a String. + def initialize + @metrics = [] + @uuid = SecureRandom.uuid + + @started_at = nil + @finished_at = nil + + @tags = {} + end + + def duration + @finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0 + end + + def run + Thread.current[THREAD_KEY] = self + + @started_at = Time.now + + yield + ensure + @finished_at = Time.now + + Thread.current[THREAD_KEY] = nil + end + + def add_metric(series, values, tags = {}) + tags = tags.merge(transaction_id: @uuid) + + @metrics << Metric.new(series, values, tags) + end + + def add_tag(key, value) + @tags[key] = value + end + + def finish + track_self + submit + end + + def track_self + add_metric(SERIES, { duration: duration }, @tags) + end + + def submit + Metrics.submit_metrics(@metrics.map(&:to_hash)) + end + end + end +end diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb new file mode 100644 index 0000000000000000000000000000000000000000..f33bfd0bd0e684e6ca8ba31796b1878889b2c8f1 --- /dev/null +++ b/lib/gitlab/o_auth/session.rb @@ -0,0 +1,17 @@ +module Gitlab + module OAuth + module Session + def self.create(provider, ticket) + Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration) + end + + def self.destroy(provider, ticket) + Rails.cache.delete("gitlab:#{provider}:#{ticket}") + end + + def self.valid?(provider, ticket) + Rails.cache.read("gitlab:#{provider}:#{ticket}").present? + end + end + end +end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 17ce4d4b1747e7004ed7e628b5c01f85008df745..f1a362f5303a296099c4ed7c725312b9498605a1 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -64,7 +64,7 @@ module Gitlab # If a corresponding person exists with same uid in a LDAP server, # set up a Gitlab user with dual LDAP and Omniauth identities. - if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn.downcase, ldap_person.provider) + if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) else diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb new file mode 100644 index 0000000000000000000000000000000000000000..70e7f25d518f6f62d0b2c969d2c5e104829d707a --- /dev/null +++ b/lib/gitlab/recaptcha.rb @@ -0,0 +1,14 @@ +module Gitlab + module Recaptcha + def self.load_configurations! + if current_application_settings.recaptcha_enabled + ::Recaptcha.configure do |config| + config.public_key = current_application_settings.recaptcha_site_key + config.private_key = current_application_settings.recaptcha_private_key + end + + true + end + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 42f7c26f3c4a10a8d0b0834c81daa2beab2f2bd5..be795649e59ab6015f114cb196e4c95ad902a58f 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -3,11 +3,12 @@ require 'banzai' module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor - attr_accessor :project, :current_user + attr_accessor :project, :current_user, :author - def initialize(project, current_user = nil) + def initialize(project, current_user = nil, author = nil) @project = project @current_user = current_user + @author = author @references = {} @@ -18,10 +19,24 @@ module Gitlab super(text, context.merge(project: project)) end - %i(user label issue merge_request snippet commit commit_range).each do |type| + %i(user label merge_request snippet commit commit_range).each do |type| define_method("#{type}s") do - @references[type] ||= references(type, project: project, current_user: current_user) + @references[type] ||= references(type, reference_context) end end + + def issues + if project && project.jira_tracker? + @references[:external_issue] ||= references(:external_issue, reference_context) + else + @references[:issue] ||= references(:issue, reference_context) + end + end + + private + + def reference_context + { project: project, current_user: current_user, author: author } + end end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 335dc44be19d2cee966cbb6d407d11c0f4db23d4..3160a3c7582547aa117bae1285e0e3ce9424e2d4 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -51,6 +51,15 @@ module Gitlab def allowed_fork_levels(origin_level) [PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level } end + + def level_name(level) + level_name = 'Unknown' + options.each do |name, lvl| + level_name = name if lvl == level.to_i + end + + level_name + end end def private? diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 6762ca47c328cc0db82d85f7b220b5f356b1cc15..8c309efc7b8fc58c227fc21e51b01b01fa3efb27 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -39,7 +39,7 @@ module Rouge lineanchorsid: 'L', anchorlinenos: false, inline_theme: nil - ) + ) @nowrap = nowrap @cssclass = cssclass @linenos = linenos diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 43fda6fa92e59f8d3f3302be51f36b4e8ceee9ec..c5f07c8b508a948796901b0cf6bcc5024360ba71 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -33,12 +33,13 @@ app_user="git" app_root="/home/$app_user/gitlab" pid_path="$app_root/tmp/pids" socket_path="$app_root/tmp/sockets" +rails_socket="$socket_path/gitlab.socket" web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" mail_room_enabled=false mail_room_pid_path="$pid_path/mail_room.pid" gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" -gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080" +gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" shell_path="/bin/bash" @@ -91,7 +92,7 @@ check_pids(){ ## Called when we have started the two processes and are waiting for their pid files. wait_for_pids(){ - # We are sleeping a bit here mostly because sidekiq is slow at writing it's pid + # We are sleeping a bit here mostly because sidekiq is slow at writing its pid i=0; while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do sleep 0.1; @@ -107,7 +108,7 @@ wait_for_pids(){ } # We use the pids in so many parts of the script it makes sense to always check them. -# Only after start() is run should the pids change. Sidekiq sets it's own pid. +# Only after start() is run should the pids change. Sidekiq sets its own pid. check_pids @@ -289,7 +290,7 @@ stop_gitlab() { sleep 1 # Cleaning up unused pids rm "$web_server_pid_path" 2>/dev/null - # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid. + # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up its own pid. rm -f "$gitlab_workhorse_pid_path" if [ "$mail_room_enabled" = true ]; then rm "$mail_room_pid_path" 2>/dev/null @@ -298,7 +299,7 @@ stop_gitlab() { print_status } -## Prints the status of GitLab and it's components. +## Prints the status of GitLab and its components. print_status() { check_status if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then @@ -332,7 +333,7 @@ print_status() { fi } -## Tells unicorn to reload it's config and Sidekiq to restart +## Tells unicorn to reload its config and Sidekiq to restart reload_gitlab(){ exit_if_not_running if [ "$wpid" = "0" ];then diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 79ae8e0ae55abf2fb3fe76dd692b1b4fe9dd3462..1937ca582b0548939c42d2aca1bab39b45357b2a 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -9,11 +9,11 @@ RAILS_ENV="production" # The default is "git". app_user="git" -# app_root defines the folder in which gitlab and it's components are installed. +# app_root defines the folder in which gitlab and its components are installed. # The default is "/home/$app_user/gitlab" app_root="/home/$app_user/gitlab" -# pid_path defines a folder in which the gitlab and it's components place their pids. +# pid_path defines a folder in which the gitlab and its components place their pids. # This variable is also used below to define the relevant pids for the gitlab components. # The default is "$app_root/tmp/pids" pid_path="$app_root/tmp/pids" @@ -36,7 +36,7 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" # '-listenNetwork tcp -listenAddr localhost:8181'. # The -authBackend setting tells gitlab-workhorse where it can reach # Unicorn. -gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080" +gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 2a79fbdcf93c8fddfd1c487318f88c09d0bbaf2e..fc5475c4eef881b2b67e8bcc231f622144b93fa0 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -10,34 +10,12 @@ ## If you change this file in a Merge Request, please also create ## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests ## -################################## -## CHUNKED TRANSFER ## -################################## -## -## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0] -## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object -## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get -## around this by tweaking this configuration file and either: -## - installing an old version of Nginx with the chunkin module [2] compiled in, or -## - using a newer version of Nginx. -## -## At the time of writing we do not know if either of these theoretical solutions works. -## As a workaround users can use Git over SSH to push large files. -## -## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 -## [1] https://github.com/agentzh/chunkin-nginx-module#status -## [2] https://github.com/agentzh/chunkin-nginx-module -## ################################### ## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. -upstream gitlab { - server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; -} - upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -54,10 +32,6 @@ server { server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; - ## Increase this if you want to upload large attachments - ## Or if you want to accept large git objects over http - client_max_body_size 20m; - ## See app/controllers/application_controller.rb for headers set ## Individual nginx logs for this GitLab vhost @@ -65,103 +39,8 @@ server { error_log /var/log/nginx/gitlab_error.log; location / { - ## Serve static files from defined root folder. - ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri /index.html $uri.html @gitlab; - } - - ## We route uploads through GitLab to prevent XSS and enforce access control. - location /uploads/ { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - ## If a file, which is not found in the root folder is requested, - ## then the proxy passes the request to the upsteam (gitlab unicorn). - location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/api/v3/projects/.*/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location @gitlab-workhorse { - client_max_body_size 0; - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; + gzip off; ## https://github.com/gitlabhq/gitlabhq/issues/694 ## Some requests take more than 30 seconds. @@ -169,14 +48,7 @@ server { proxy_connect_timeout 300; proxy_redirect off; - # Do not buffer Git HTTP responses - proxy_buffering off; - - # The following settings only work with NGINX 1.7.11 or newer - # - # # Pass chunked request bodies to gitlab-workhorse as-is - # proxy_request_buffering off; - # proxy_http_version 1.1; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; @@ -185,18 +57,4 @@ server { proxy_pass http://gitlab-workhorse; } - - ## Enable gzip compression as per rails guide: - ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression - ## WARNING: If you are using relative urls remove the block below - ## See config/application.rb under "Relative url support" for the list of - ## other files that need to be changed for relative url support - location ~ ^/(assets)/ { - root /home/git/gitlab/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; - } - - error_page 502 /502.html; } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 79fe1474821293ab2be6c743d8b679633141f9cd..1e5f85413ec6dd8d124c4795f3b9dbfa32acfa86 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -14,34 +14,12 @@ ## If you change this file in a Merge Request, please also create ## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests ## -################################## -## CHUNKED TRANSFER ## -################################## -## -## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0] -## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object -## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get -## around this by tweaking this configuration file and either: -## - installing an old version of Nginx with the chunkin module [2] compiled in, or -## - using a newer version of Nginx. -## -## At the time of writing we do not know if either of these theoretical solutions works. -## As a workaround users can use Git over SSH to push large files. -## -## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99 -## [1] https://github.com/agentzh/chunkin-nginx-module#status -## [2] https://github.com/agentzh/chunkin-nginx-module -## ################################### ## configuration ## ################################### ## ## See installation.md#using-https for additional HTTPS configuration details. -upstream gitlab { - server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; -} - upstream gitlab-workhorse { server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0; } @@ -61,7 +39,6 @@ server { error_log /var/log/nginx/gitlab_error.log; } - ## HTTPS host server { listen 0.0.0.0:443 ssl; @@ -70,10 +47,6 @@ server { server_tokens off; ## Don't show the nginx version number, a security best practice root /home/git/gitlab/public; - ## Increase this if you want to upload large attachments - ## Or if you want to accept large git objects over http - client_max_body_size 20m; - ## Strong SSL Security ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ ssl on; @@ -110,104 +83,7 @@ server { error_log /var/log/nginx/gitlab_error.log; location / { - ## Serve static files from defined root folder. - ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri /index.html $uri.html @gitlab; - } - - ## We route uploads through GitLab to prevent XSS and enforce access control. - location /uploads/ { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - ## If a file, which is not found in the root folder is requested, - ## then the proxy passes the request to the upsteam (gitlab unicorn). - location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - - proxy_pass http://gitlab; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location ~ ^/api/v3/projects/.*/repository/archive { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - # Build artifacts should be submitted to this location - location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; - } - - location @gitlab-workhorse { - client_max_body_size 0; - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. gzip off; ## https://github.com/gitlabhq/gitlabhq/issues/694 @@ -216,14 +92,7 @@ server { proxy_connect_timeout 300; proxy_redirect off; - # Do not buffer Git HTTP responses - proxy_buffering off; - - # The following settings only work with NGINX 1.7.11 or newer - # - # # Pass chunked request bodies to gitlab-workhorse as-is - # proxy_request_buffering off; - # proxy_http_version 1.1; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; @@ -232,18 +101,4 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://gitlab-workhorse; } - - ## Enable gzip compression as per rails guide: - ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression - ## WARNING: If you are using relative urls remove the block below - ## See config/application.rb under "Relative url support" for the list of - ## other files that need to be changed for relative url support - location ~ ^/(assets)/ { - root /home/git/gitlab/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; - } - - error_page 502 /502.html; } diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index a474574c6e5a40284b9a57e9d6ad9ffdd44b81a5..e74731c9ed82b07093aa3718306dbeac5ce9bebe 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -98,7 +98,7 @@ describe Projects::TreeController do project_id: project.to_param, id: 'master', dir_name: path, - new_branch: target_branch, + target_branch: target_branch, commit_message: 'Test commit message') end @@ -108,8 +108,8 @@ describe Projects::TreeController do it 'redirects to the new directory' do expect(subject). - to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}") - expect(flash[:notice]).to eq('The directory has been successfully created') + to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}") + expect(flash[:notice]).to eq('The directory has been successfully created.') end end @@ -119,7 +119,7 @@ describe Projects::TreeController do it 'does not allow overwriting of existing files' do expect(subject). - to redirect_to("/#{project.path_with_namespace}/blob/master") + to redirect_to("/#{project.path_with_namespace}/tree/master") expect(flash[:alert]).to eq('Directory already exists as a file') end end diff --git a/spec/factories.rb b/spec/factories.rb index 4bf93adabe27c31e5977daa3eb85726ce88e2a17..d6b4efa9a03d78b858ccf43b2e2d0cc54e57c9c0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -43,7 +43,8 @@ FactoryGirl.define do end after(:create) do |user, evaluator| - user.identities << create(:identity, + user.identities << create( + :identity, provider: evaluator.provider, extern_uid: evaluator.extern_uid ) diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 66a2cc0c157a90737ab32cdb29055552d926079e..26d03944b8a24dd811e1a6da96f05b73118ae215 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -63,7 +63,7 @@ describe "Admin Runners" do end describe 'runners registration token' do - let!(:token) { current_application_settings.ensure_runners_registration_token } + let!(:token) { current_application_settings.runners_registration_token } before { visit admin_runners_path } it 'has a registration token' do diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e6e73e5e67ca34dd3ea2734fc30842bdf0d9c6a2 --- /dev/null +++ b/spec/features/ci_lint_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'CI Lint' do + before do + login_as :user + end + + describe 'YAML parsing' do + before do + visit ci_lint_path + fill_in 'content', with: yaml_content + click_on 'Validate' + end + + context 'YAML is correct' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'Yaml parsing' do + within "table" do + expect(page).to have_content('Job - rspec') + expect(page).to have_content('Job - spinach') + expect(page).to have_content('Deploy Job - staging') + expect(page).to have_content('Deploy Job - production') + end + end + end + + context 'YAML is incorrect' do + let(:yaml_content) { '' } + + it 'displays information about an error' do + expect(page).to have_content('Status: syntax is incorrect') + expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml') + end + end + end +end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index ecc85376ffc22067694922833748d524f113d5ad..fe7f07f5b75017293b4772d71c7ecd746a2da46c 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -19,30 +19,13 @@ describe 'Commits' do let!(:build) { FactoryGirl.create :ci_build, commit: commit } describe 'Project commits' do - context 'builds enabled' do - context '.gitlab-ci.yml found' do - before do - visit namespace_project_commits_path(project.namespace, project, :master) - end - - it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_css(".ci-status-link") - end - end - end + before do + visit namespace_project_commits_path(project.namespace, project, :master) + end - context 'no .gitlab-ci.yml found' do - before do - stub_ci_commit_yaml_file(nil) - visit namespace_project_commits_path(project.namespace, project, :master) - end - - it 'should not show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_no_css(".ci-status-link") - end - end + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") end end end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index f600f8684acfcf2eeec6a5e73bc1c964fb2b4498..38c8d343ce3bd79b1462905104e464fbebec152f 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -13,7 +13,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::None.title) - expect(page).to have_css('.issue-title', count: 1) + expect(page).to have_css('.title', count: 1) end scenario 'filters by a specific Milestone', js: true do @@ -23,7 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) - expect(page).to have_css('.issue-title', count: 1) + expect(page).to have_css('.title', count: 1) end def visit_issues(project) diff --git a/spec/features/lint_spec.rb b/spec/features/lint_spec.rb deleted file mode 100644 index 5d8f56e2cfb160bac8f81f0e6a41e276d0a7713a..0000000000000000000000000000000000000000 --- a/spec/features/lint_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe "Lint" do - before do - login_as :user - end - - it "Yaml parsing", js: true do - content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - visit ci_lint_path - fill_in "content", with: content - click_on "Validate" - within "table" do - expect(page).to have_content("Job - rspec") - expect(page).to have_content("Job - spinach") - expect(page).to have_content("Deploy Job - staging") - expect(page).to have_content("Deploy Job - production") - end - end - - it "Yaml parsing with error", js: true do - visit ci_lint_path - fill_in "content", with: "" - click_on "Validate" - expect(page).to have_content("Status: syntax is incorrect") - expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml") - end -end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 922c76285d1865baca9aa06007aab96e9985a7df..2451e56fe7ce6338b3863e06dee98c2351bec2bc 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -98,4 +98,56 @@ feature 'Login', feature: true do expect(page).to have_content('Invalid login or password.') end end + + describe 'with required two-factor authentication enabled' do + let(:user) { create(:user) } + before(:each) { stub_application_setting(require_two_factor_authentication: true) } + + context 'with grace period defined' do + before(:each) do + stub_application_setting(two_factor_grace_period: 48) + login_with(user) + end + + context 'within the grace period' do + it 'redirects to two-factor configuration page' do + expect(current_path).to eq new_profile_two_factor_auth_path + expect(page).to have_content('You must configure Two-Factor Authentication in your account until') + end + + it 'two-factor configuration is skippable' do + expect(current_path).to eq new_profile_two_factor_auth_path + + click_link 'Configure it later' + expect(current_path).to eq root_path + end + end + + context 'after the grace period' do + let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } + + it 'redirects to two-factor configuration page' do + expect(current_path).to eq new_profile_two_factor_auth_path + expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + end + + it 'two-factor configuration is not skippable' do + expect(current_path).to eq new_profile_two_factor_auth_path + expect(page).not_to have_link('Configure it later') + end + end + end + + context 'without grace pariod defined' do + before(:each) do + stub_application_setting(two_factor_grace_period: 0) + login_with(user) + end + + it 'redirects to two-factor configuration page' do + expect(current_path).to eq new_profile_two_factor_auth_path + expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + end + end + end end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 28a46a0725d81c2e464607228f5ab52f8d30c7b9..7aa7eb965e908ee173aeda4fff7b815414389a2d 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -21,12 +21,12 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end it 'displays the Merge When Build Succeeds button' do - expect(page).to have_link "Merge When Build Succeeds" + expect(page).to have_button "Merge When Build Succeeds" end context "Merge When Build succeeds enabled" do before do - click_link "Merge When Build Succeeds" + click_button "Merge When Build Succeeds" end it 'activates Merge When Build Succeeds feature' do @@ -58,7 +58,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do it 'cancels the automatic merge' do click_link "Cancel Automatic Merge" - expect(page).to have_link "Merge When Build Succeeds" + expect(page).to have_button "Merge When Build Succeeds" visit_merge_request(merge_request) # Needed to refresh the page expect(page).to have_content "Canceled the automatic merge" diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 09fcff2444ac83771442c68f0935fd591fdd4305..74b148f5d178b44fb55aa3340cbca7f4fefc5180 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -70,6 +70,20 @@ feature 'Project', feature: true do end end + describe 'leave project link' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + login_with(user) + project.team.add_user(user, Gitlab::Access::MASTER) + visit namespace_project_path(project.namespace, project) + end + + it { expect(page).to have_content('You have Master access to this project.') } + it { expect(page).to have_link('Leave this project') } + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb index 4b78e3a61f04ebeea7ffd9953bcccf7da94b6bfd..65f8073c6933937f2264d17103af85671ab64951 100644 --- a/spec/features/security/group_access_spec.rb +++ b/spec/features/security/group_access_spec.rb @@ -16,11 +16,11 @@ describe 'Group access', feature: true do end end - def group_member(access_level, group = group) + def group_member(access_level, grp = group()) level = Object.const_get("Gitlab::Access::#{access_level.upcase}") create(:user).tap do |user| - group.add_user(user, level) + grp.add_user(user, level) end end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index fca3c77fc64464e9e8a1fda7d31c237b7c699902..b7368cca29d0cdb3dbfcca4f7910718866e10cc7 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -47,7 +47,7 @@ feature 'Task Lists', feature: true do it 'contains the required selectors' do visit_issue(project, issue) - container = '.issue-details .description.js-task-list-container' + container = '.detail-page-description .description.js-task-list-container' expect(page).to have_selector(container) expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") @@ -123,7 +123,7 @@ feature 'Task Lists', feature: true do it 'contains the required selectors' do visit_merge_request(project, merge) - container = '.merge-request-details .description.js-task-list-container' + container = '.detail-page-description .description.js-task-list-container' expect(page).to have_selector(container) expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5568f06639c42b83c147fe9052007ab2e3e26a84..68527c3a4f84b173bf8e30e55e81e7d6d1963cd1 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -263,11 +263,12 @@ describe ApplicationHelper do end it 'includes a default js-timeago class' do - expect(element.attr('class')).to eq 'time_ago js-timeago' + expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending' end it 'accepts a custom html_class' do - expect(element(html_class: 'custom_class').attr('class')).to eq 'custom_class js-timeago' + expect(element(html_class: 'custom_class').attr('class')). + to eq 'custom_class js-timeago js-timeago-pending' end it 'accepts a custom tooltip placement' do @@ -278,7 +279,7 @@ describe ApplicationHelper do el = element.next_element expect(el.name).to eq 'script' - expect(el.text).to include "$('.js-timeago').last().timeago()" + expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" end it 'allows the script tag to be excluded' do diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 7fc53eb1472e321927c93eb7a3ad7a667f2e2df0..4f8d9c672620ce6c64b1c092b7944ca896c1e177 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -6,13 +6,8 @@ describe CiStatusHelper do let(:success_commit) { double("Ci::Commit", status: 'success') } let(:failed_commit) { double("Ci::Commit", status: 'failed') } - describe 'ci_status_color' do - it { expect(ci_status_icon(success_commit)).to include('fa-check') } - it { expect(ci_status_icon(failed_commit)).to include('fa-close') } - end - - describe 'ci_status_color' do - it { expect(ci_status_color(success_commit)).to eq('green') } - it { expect(ci_status_color(failed_commit)).to eq('red') } + describe 'ci_status_icon' do + it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') } + it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') } end end diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb index 5d1744606818a3efbc04e15f22cc444fb49c644d..4ea90a80a926649ba2597bf08ddad0831a8c14e8 100644 --- a/spec/helpers/groups_helper.rb +++ b/spec/helpers/groups_helper.rb @@ -9,7 +9,7 @@ describe GroupsHelper do group.avatar = File.open(avatar_file_path) group.save! expect(group_icon(group.path).to_s). - to match("/uploads/group/avatar/#{ group.id }/banana_sample.gif") + to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") end it 'should give default avatar_icon when no avatar is present' do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 1f2c4ee77b59fdf79c94f5295f3c68048f5136c3..ffd8ebae029726441a736585a9dd7dae48678fac 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -127,18 +127,6 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "#url_to_emoji" do - it "returns url" do - expect(url_to_emoji("smile")).to include("emoji/1F604.png") - end - end - - describe "#emoji_list" do - it "returns url" do - expect(emoji_list).to be_kind_of(Array) - end - end - describe "#note_active_class" do before do @note = create :note @@ -153,4 +141,11 @@ describe IssuesHelper do expect(note_active_class(Note.all, @note.author)).to eq("active") end end + + describe "#awards_sort" do + it "sorts a hash so thumbsup and thumbsdown are always on top" do + data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" } + expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"]) + end + end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 0ef1efb8bce1ee6c01105aab7be74fd62b8af73a..600e1c4e9ecf1727403f68d7cca18bb270be25d7 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,24 +1,57 @@ require 'spec_helper' describe MergeRequestsHelper do - describe "#issues_sentence" do + describe 'ci_build_details_path' do + let(:project) { create :project } + let(:merge_request) { MergeRequest.new } + let(:ci_service) { CiService.new } + let(:last_commit) { Ci::Commit.new({}) } + + before do + allow(merge_request).to receive(:source_project).and_return(project) + allow(merge_request).to receive(:last_commit).and_return(last_commit) + allow(project).to receive(:ci_service).and_return(ci_service) + allow(last_commit).to receive(:sha).and_return('12d65c') + end + + it 'does not include api credentials in a link' do + allow(ci_service). + to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") + expect(helper.ci_build_details_path(merge_request)).to_not match("secret") + end + end + + describe '#issues_sentence' do subject { issues_sentence(issues) } let(:issues) do [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] end it { is_expected.to eq('#1, #2, and #3') } + + context 'for JIRA issues' do + let(:project) { create(:project) } + let(:issues) do + [ + JiraIssue.new('JIRA-123', project), + JiraIssue.new('JIRA-456', project), + JiraIssue.new('FOOBAR-7890', project) + ] + end + + it { is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456') } + end end - describe "#format_mr_branch_names" do - describe "within the same project" do + describe '#format_mr_branch_names' do + describe 'within the same project' do let(:merge_request) { create(:merge_request) } subject { format_mr_branch_names(merge_request) } it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) } end - describe "within different projects" do + describe 'within different projects' do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd7107779f6cd99b2d5f3349be12573025811062 --- /dev/null +++ b/spec/helpers/page_layout_helper_spec.rb @@ -0,0 +1,129 @@ +require 'rails_helper' + +describe PageLayoutHelper do + describe 'page_description' do + it 'defaults to value returned by page_description_default helper' do + allow(helper).to receive(:page_description_default).and_return('Foo') + + expect(helper.page_description).to eq 'Foo' + end + + it 'returns the last-pushed description' do + helper.page_description('Foo') + helper.page_description('Bar') + helper.page_description('Baz') + + expect(helper.page_description).to eq 'Baz' + end + + it 'squishes multiple newlines' do + helper.page_description("Foo\nBar\nBaz") + + expect(helper.page_description).to eq 'Foo Bar Baz' + end + + it 'truncates' do + helper.page_description <<-LOREM.strip_heredoc + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo + ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Donec quam felis, + ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa + quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, + arcu. + LOREM + + expect(helper.page_description).to end_with 'quam felis,...' + end + + it 'sanitizes all HTML' do + helper.page_description("<b>Bold</b> <h1>Header</h1>") + + expect(helper.page_description).to eq 'Bold Header' + end + end + + describe 'page_description_default' do + it 'uses Project description when available' do + project = double(description: 'Project Description') + helper.instance_variable_set(:@project, project) + + expect(helper.page_description_default).to eq 'Project Description' + end + + it 'uses brand_title when Project description is nil' do + project = double(description: nil) + helper.instance_variable_set(:@project, project) + + expect(helper).to receive(:brand_title).and_return('Brand Title') + expect(helper.page_description_default).to eq 'Brand Title' + end + + it 'falls back to brand_title' do + allow(helper).to receive(:brand_title).and_return('Brand Title') + + expect(helper.page_description_default).to eq 'Brand Title' + end + end + + describe 'page_image' do + it 'defaults to the GitLab logo' do + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end + + context 'with @project' do + it 'uses Project avatar if available' do + project = double(avatar_url: 'http://example.com/uploads/avatar.png') + helper.instance_variable_set(:@project, project) + + expect(helper.page_image).to eq project.avatar_url + end + + it 'falls back to the default' do + project = double(avatar_url: nil) + helper.instance_variable_set(:@project, project) + + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end + end + + context 'with @user' do + it 'delegates to avatar_icon helper' do + user = double('User') + helper.instance_variable_set(:@user, user) + + expect(helper).to receive(:avatar_icon).with(user) + + helper.page_image + end + end + end + + describe 'page_card_attributes' do + it 'raises ArgumentError when given more than two attributes' do + map = { foo: 'foo', bar: 'bar', baz: 'baz' } + + expect { helper.page_card_attributes(map) }. + to raise_error(ArgumentError, /more than two attributes/) + end + + it 'rejects blank values' do + map = { foo: 'foo', bar: '' } + helper.page_card_attributes(map) + + expect(helper.page_card_attributes).to eq({ foo: 'foo' }) + end + end + + describe 'page_card_meta_tags' do + it 'returns the twitter:label and twitter:data tags' do + allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar') + + tags = helper.page_card_meta_tags + + aggregate_failures do + expect(tags).to include %q(<meta property="twitter:label1" content="foo" />) + expect(tags).to include %q(<meta property="twitter:data1" content="bar" />) + end + end + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index f2efb528aeb6cc1c9397e7a5c9a8676286f4085d..53207767581e906005768c4e3313ea0beca51b35 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -53,6 +53,16 @@ describe ProjectsHelper do end end + describe 'user_max_access_in_project' do + let(:project) { create(:project) } + let(:user) { create(:user) } + before do + project.team.add_user(user, Gitlab::Access::MASTER) + end + + it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') } + end + describe "readme_cache_key" do let(:project) { create(:project) } diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml index 7e8b2a64351f176907213128c04d0f4af597ae0c..470cabeafbb90718ad18d14423ef334ac3c61894 100644 --- a/spec/javascripts/fixtures/issues_show.html.haml +++ b/spec/javascripts/fixtures/issues_show.html.haml @@ -1,6 +1,16 @@ -%a.btn-close +:css + .hidden { display: none !important; } -.issue-details +.flash-container + .flash-alert + .flash-notice + +.status-box.status-box-open Open +.status-box.status-box-closed.hidden Closed +%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close +%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen + +.detail-page-description .description.js-task-list-container .wiki %ul.task-list diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml index f0c622935f849cc2680fa27b3534ec035b05c76b..8447dfdda3205e6ac5b25a6cb25da8ba7ba3bffa 100644 --- a/spec/javascripts/fixtures/merge_requests_show.html.haml +++ b/spec/javascripts/fixtures/merge_requests_show.html.haml @@ -1,6 +1,6 @@ %a.btn-close -.merge-request-details +.detail-page-description .description.js-task-list-container .wiki %ul.task-list diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..f06629e5eccd4ef5814c12e510479c3f0648af8d --- /dev/null +++ b/spec/javascripts/fixtures/new_branch.html.haml @@ -0,0 +1,4 @@ +%form.js-create-branch-form + %input.js-branch-name + .js-branch-name-error + %input{id: "ref"} diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 268e4c68c3150f230ffa44a949fcd2320ccd384d..7e67c77886109beb2b56c8b5c481744f16af3e16 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -20,3 +20,89 @@ describe 'Issue', -> expect(req.data.issue.description).not.toBe(null) $('.js-task-list-field').trigger('tasklist:changed') +describe 'reopen/close issue', -> + fixture.preload('issues_show.html') + beforeEach -> + fixture.load('issues_show.html') + @issue = new Issue() + it 'closes an issue', -> + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://gitlab.com/issues/6/close') + obj.success saved: true + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeVisible() + expect($btnClose).toBeHidden() + expect($('div.status-box-closed')).toBeVisible() + expect($('div.status-box-open')).toBeHidden() + + it 'fails to closes an issue with success:false', -> + + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://goesnowhere.nothing/whereami') + obj.success saved: false + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + $btnClose.attr('href','http://goesnowhere.nothing/whereami') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() + expect($('div.status-box-open')).toBeVisible() + expect($('div.flash-alert')).toBeVisible() + expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') + + it 'fails to closes an issue with HTTP error', -> + + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://goesnowhere.nothing/whereami') + obj.error() + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + $btnClose.attr('href','http://goesnowhere.nothing/whereami') + expect($btnReopen).toBeHidden() + expect($btnClose.text()).toBe('Close') + expect(typeof $btnClose.prop('disabled')).toBe('undefined') + + $btnClose.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() + expect($('div.status-box-open')).toBeVisible() + expect($('div.flash-alert')).toBeVisible() + expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') + + it 'reopens an issue', -> + $.ajax = (obj) -> + expect(obj.type).toBe('PUT') + expect(obj.url).toBe('http://gitlab.com/issues/6/reopen') + obj.success saved: true + + $btnClose = $('a.btn-close') + $btnReopen = $('a.btn-reopen') + expect($btnReopen.text()).toBe('Reopen') + + $btnReopen.trigger('click') + + expect($btnReopen).toBeHidden() + expect($btnClose).toBeVisible() + expect($('div.status-box-open')).toBeVisible() + expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..f2ce85efcdcbbaac28c81ec75867d3723f587833 --- /dev/null +++ b/spec/javascripts/new_branch_spec.js.coffee @@ -0,0 +1,160 @@ +#= require jquery-ui +#= require new_branch_form + +describe 'Branch', -> + describe 'create a new branch', -> + fixture.preload('new_branch.html') + + fillNameWith = (value) -> + $('.js-branch-name').val(value).trigger('blur') + + expectToHaveError = (error) -> + expect($('.js-branch-name-error span').text()).toEqual(error) + + beforeEach -> + fixture.load('new_branch.html') + $('form').on 'submit', (e) -> e.preventDefault() + + @form = new NewBranchForm($('.js-create-branch-form'), []) + + it "can't start with a dot", -> + fillNameWith '.foo' + expectToHaveError "can't start with '.'" + + it "can't start with a slash", -> + fillNameWith '/foo' + expectToHaveError "can't start with '/'" + + it "can't have two consecutive dots", -> + fillNameWith 'foo..bar' + expectToHaveError "can't contain '..'" + + it "can't have spaces anywhere", -> + fillNameWith ' foo' + expectToHaveError "can't contain spaces" + fillNameWith 'foo bar' + expectToHaveError "can't contain spaces" + fillNameWith 'foo ' + expectToHaveError "can't contain spaces" + + it "can't have ~ anywhere", -> + fillNameWith '~foo' + expectToHaveError "can't contain '~'" + fillNameWith 'foo~bar' + expectToHaveError "can't contain '~'" + fillNameWith 'foo~' + expectToHaveError "can't contain '~'" + + it "can't have tilde anwhere", -> + fillNameWith '~foo' + expectToHaveError "can't contain '~'" + fillNameWith 'foo~bar' + expectToHaveError "can't contain '~'" + fillNameWith 'foo~' + expectToHaveError "can't contain '~'" + + it "can't have caret anywhere", -> + fillNameWith '^foo' + expectToHaveError "can't contain '^'" + fillNameWith 'foo^bar' + expectToHaveError "can't contain '^'" + fillNameWith 'foo^' + expectToHaveError "can't contain '^'" + + it "can't have : anywhere", -> + fillNameWith ':foo' + expectToHaveError "can't contain ':'" + fillNameWith 'foo:bar' + expectToHaveError "can't contain ':'" + fillNameWith ':foo' + expectToHaveError "can't contain ':'" + + it "can't have question mark anywhere", -> + fillNameWith '?foo' + expectToHaveError "can't contain '?'" + fillNameWith 'foo?bar' + expectToHaveError "can't contain '?'" + fillNameWith 'foo?' + expectToHaveError "can't contain '?'" + + it "can't have asterisk anywhere", -> + fillNameWith '*foo' + expectToHaveError "can't contain '*'" + fillNameWith 'foo*bar' + expectToHaveError "can't contain '*'" + fillNameWith 'foo*' + expectToHaveError "can't contain '*'" + + it "can't have open bracket anywhere", -> + fillNameWith '[foo' + expectToHaveError "can't contain '['" + fillNameWith 'foo[bar' + expectToHaveError "can't contain '['" + fillNameWith 'foo[' + expectToHaveError "can't contain '['" + + it "can't have a backslash anywhere", -> + fillNameWith '\\foo' + expectToHaveError "can't contain '\\'" + fillNameWith 'foo\\bar' + expectToHaveError "can't contain '\\'" + fillNameWith 'foo\\' + expectToHaveError "can't contain '\\'" + + it "can't contain a sequence @{ anywhere", -> + fillNameWith '@{foo' + expectToHaveError "can't contain '@{'" + fillNameWith 'foo@{bar' + expectToHaveError "can't contain '@{'" + fillNameWith 'foo@{' + expectToHaveError "can't contain '@{'" + + it "can't have consecutive slashes", -> + fillNameWith 'foo//bar' + expectToHaveError "can't contain consecutive slashes" + + it "can't end with a slash", -> + fillNameWith 'foo/' + expectToHaveError "can't end in '/'" + + it "can't end with a dot", -> + fillNameWith 'foo.' + expectToHaveError "can't end in '.'" + + it "can't end with .lock", -> + fillNameWith 'foo.lock' + expectToHaveError "can't end in '.lock'" + + it "can't be the single character @", -> + fillNameWith '@' + expectToHaveError "can't be '@'" + + it "concatenates all error messages", -> + fillNameWith '/foo bar?~.' + expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'" + + it "doesn't duplicate error messages", -> + fillNameWith '?foo?bar?zoo?' + expectToHaveError "can't contain '?'" + + it "removes the error message when is a valid name", -> + fillNameWith 'foo?bar' + expect($('.js-branch-name-error span').length).toEqual(1) + fillNameWith 'foobar' + expect($('.js-branch-name-error span').length).toEqual(0) + + it "can have dashes anywhere", -> + fillNameWith '-foo-bar-zoo-' + expect($('.js-branch-name-error span').length).toEqual(0) + + it "can have underscores anywhere", -> + fillNameWith '_foo_bar_zoo_' + expect($('.js-branch-name-error span').length).toEqual(0) + + it "can have numbers anywhere", -> + fillNameWith '1foo2bar3zoo4' + expect($('.js-branch-name-error span').length).toEqual(0) + + it "can be only letters", -> + fillNameWith 'foo' + expect($('.js-branch-name-error span').length).toEqual(0) diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 3534bf977846c9ea62dbfcaa3c7eecf40c01c76e..8bdebae1841bdf6782c8ff98199ffda81bf16cf6 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -37,9 +37,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do .to eq urls.namespace_project_url(project.namespace, project) end - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [project.creator] + context "when the author is a member of the project" do + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}", author: project.creator) + expect(result[:references][:user]).to eq [project.creator] + end + end + + context "when the author is not a member of the project" do + + let(:other_user) { create(:user) } + + it "doesn't add to the results hash" do + result = reference_pipeline_result("Hey #{reference}", author: other_user) + expect(result[:references][:user]).to eq [] + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c90133fbf03763c89628ff322bc88692d67fddf4..d15100fc6d8c075de90677c6129d4bb0d24c8fcc 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } - + describe "#builds_for_ref" do let(:type) { 'test' } @@ -29,7 +29,7 @@ module Ci when: "on_success" }) end - + describe :only do it "does not return builds if only has another branch" do config = YAML.dump({ @@ -517,7 +517,7 @@ module Ci end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") end - it "returns errors if there is no any jobs defined" do + it "returns errors if there are no jobs defined" do config = YAML.dump({ before_script: ["bundle update"] }) expect do GitlabCiYamlProcessor.new(config, path) diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 3bba5e2efa2c930c4ee406232c1e9349e72162a2..1e755259dae4a4bdb85e0f1eccd5ff8ccebefa23 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -42,6 +42,21 @@ describe Gitlab::LDAP::User, lib: true do end end + describe '.find_by_uid_and_provider' do + it 'retrieves the correct user' do + special_info = { + name: 'John Ã…ström', + email: 'john@example.com', + nickname: 'jastrom' + } + special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Ã…ström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info) + special_chars_user = described_class.new(special_hash) + user = special_chars_user.save + + expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user + end + end + describe :find_or_create do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..718387cdee1785a272c5b34b53de87f09f97e4c6 --- /dev/null +++ b/spec/lib/gitlab/metrics/delta_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Delta do + let(:delta) { described_class.new } + + describe '#compared_with' do + it 'returns the delta as a Numeric' do + expect(delta.compared_with(5)).to eq(5) + end + + it 'bases the delta on a previously used value' do + expect(delta.compared_with(5)).to eq(5) + expect(delta.compared_with(15)).to eq(10) + end + end +end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a7eab9d11cc76f531a5efe03b1a739c56455fc2a --- /dev/null +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -0,0 +1,234 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Instrumentation do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + before do + @dummy = Class.new do + def self.foo(text = 'foo') + text + end + + def bar(text = 'bar') + text + end + end + + allow(@dummy).to receive(:name).and_return('Dummy') + end + + describe '.configure' do + it 'yields self' do + described_class.configure do |c| + expect(c).to eq(described_class) + end + end + end + + describe '.instrument_method' do + describe 'with metrics enabled' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) + + described_class.instrument_method(@dummy, :foo) + end + + it 'renames the original method' do + expect(@dummy).to respond_to(:_original_foo) + end + + it 'calls the instrumented method with the correct arguments' do + expect(@dummy.foo).to eq('foo') + end + + it 'tracks the call duration upon calling the method' do + allow(Gitlab::Metrics).to receive(:method_call_threshold). + and_return(0) + + allow(described_class).to receive(:transaction). + and_return(transaction) + + expect(transaction).to receive(:add_metric). + with(described_class::SERIES, an_instance_of(Hash), + method: 'Dummy.foo') + + @dummy.foo + end + + it 'does not track method calls below a given duration threshold' do + allow(Gitlab::Metrics).to receive(:method_call_threshold). + and_return(100) + + expect(transaction).to_not receive(:add_metric) + + @dummy.foo + end + end + + describe 'with metrics disabled' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(false) + end + + it 'does not instrument the method' do + described_class.instrument_method(@dummy, :foo) + + expect(@dummy).to_not respond_to(:_original_foo) + end + end + end + + describe '.instrument_instance_method' do + describe 'with metrics enabled' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) + + described_class. + instrument_instance_method(@dummy, :bar) + end + + it 'renames the original method' do + expect(@dummy.method_defined?(:_original_bar)).to eq(true) + end + + it 'calls the instrumented method with the correct arguments' do + expect(@dummy.new.bar).to eq('bar') + end + + it 'tracks the call duration upon calling the method' do + allow(Gitlab::Metrics).to receive(:method_call_threshold). + and_return(0) + + allow(described_class).to receive(:transaction). + and_return(transaction) + + expect(transaction).to receive(:add_metric). + with(described_class::SERIES, an_instance_of(Hash), + method: 'Dummy#bar') + + @dummy.new.bar + end + + it 'does not track method calls below a given duration threshold' do + allow(Gitlab::Metrics).to receive(:method_call_threshold). + and_return(100) + + expect(transaction).to_not receive(:add_metric) + + @dummy.new.bar + end + end + + describe 'with metrics disabled' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(false) + end + + it 'does not instrument the method' do + described_class. + instrument_instance_method(@dummy, :bar) + + expect(@dummy.method_defined?(:_original_bar)).to eq(false) + end + end + end + + describe '.instrument_class_hierarchy' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) + + @child1 = Class.new(@dummy) do + def self.child1_foo; end + def child1_bar; end + end + + @child2 = Class.new(@child1) do + def self.child2_foo; end + def child2_bar; end + end + end + + it 'recursively instruments a class hierarchy' do + described_class.instrument_class_hierarchy(@dummy) + + expect(@child1).to respond_to(:_original_child1_foo) + expect(@child2).to respond_to(:_original_child2_foo) + + expect(@child1.method_defined?(:_original_child1_bar)).to eq(true) + expect(@child2.method_defined?(:_original_child2_bar)).to eq(true) + end + + it 'does not instrument the root module' do + described_class.instrument_class_hierarchy(@dummy) + + expect(@dummy).to_not respond_to(:_original_foo) + expect(@dummy.method_defined?(:_original_bar)).to eq(false) + end + end + + describe '.instrument_methods' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) + end + + it 'instruments all public class methods' do + described_class.instrument_methods(@dummy) + + expect(@dummy).to respond_to(:_original_foo) + end + + it 'only instruments methods directly defined in the module' do + mod = Module.new do + def kittens + end + end + + @dummy.extend(mod) + + described_class.instrument_methods(@dummy) + + expect(@dummy).to_not respond_to(:_original_kittens) + end + + it 'can take a block to determine if a method should be instrumented' do + described_class.instrument_methods(@dummy) do + false + end + + expect(@dummy).to_not respond_to(:_original_foo) + end + end + + describe '.instrument_instance_methods' do + before do + allow(Gitlab::Metrics).to receive(:enabled?).and_return(true) + end + + it 'instruments all public instance methods' do + described_class.instrument_instance_methods(@dummy) + + expect(@dummy.method_defined?(:_original_bar)).to eq(true) + end + + it 'only instruments methods directly defined in the module' do + mod = Module.new do + def kittens + end + end + + @dummy.include(mod) + + described_class.instrument_instance_methods(@dummy) + + expect(@dummy.method_defined?(:_original_kittens)).to eq(false) + end + + it 'can take a block to determine if a method should be instrumented' do + described_class.instrument_instance_methods(@dummy) do + false + end + + expect(@dummy.method_defined?(:_original_bar)).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..aa76315c79ce48d5379a883781ff37ef48a21eb0 --- /dev/null +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Metric do + let(:metric) do + described_class.new('foo', { number: 10 }, { host: 'localtoast' }) + end + + describe '#series' do + subject { metric.series } + + it { is_expected.to eq('foo') } + end + + describe '#values' do + subject { metric.values } + + it { is_expected.to eq({ number: 10 }) } + end + + describe '#tags' do + subject { metric.tags } + + it { is_expected.to eq({ host: 'localtoast' }) } + end + + describe '#to_hash' do + it 'returns a Hash' do + expect(metric.to_hash).to be_an_instance_of(Hash) + end + + describe 'the returned Hash' do + let(:hash) { metric.to_hash } + + it 'includes the series' do + expect(hash[:series]).to eq('foo') + end + + it 'includes the tags' do + expect(hash[:tags]).to be_an_instance_of(Hash) + + expect(hash[:tags][:hostname]).to be_an_instance_of(String) + expect(hash[:tags][:process_type]).to be_an_instance_of(String) + end + + it 'includes the values' do + expect(hash[:values]).to eq({ number: 10 }) + end + + it 'includes the timestamp' do + expect(hash[:timestamp]).to be_an_instance_of(Fixnum) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2b681c9fe347b2a4f15026c84fd6240fda25795f --- /dev/null +++ b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe Gitlab::Metrics::ObfuscatedSQL do + describe '#to_s' do + it 'replaces newlines with a space' do + sql = described_class.new("SELECT x\nFROM y") + + expect(sql.to_s).to eq('SELECT x FROM y') + end + + describe 'using single values' do + it 'replaces a single integer' do + sql = described_class.new('SELECT x FROM y WHERE a = 10') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + + it 'replaces a single float' do + sql = described_class.new('SELECT x FROM y WHERE a = 10.5') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + + it 'replaces a single quoted string' do + sql = described_class.new("SELECT x FROM y WHERE a = 'foo'") + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + + if Gitlab::Database.mysql? + it 'replaces a double quoted string' do + sql = described_class.new('SELECT x FROM y WHERE a = "foo"') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + end + + it 'replaces a single regular expression' do + sql = described_class.new('SELECT x FROM y WHERE a = /foo/') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + + it 'replaces regular expressions using escaped slashes' do + sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') + end + end + + describe 'using consecutive values' do + it 'replaces multiple integers' do + sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') + end + + it 'replaces multiple floats' do + sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') + end + + it 'replaces multiple single quoted strings' do + sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')") + + expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') + end + + if Gitlab::Database.mysql? + it 'replaces multiple double quoted strings' do + sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') + end + end + + it 'replaces multiple regular expressions' do + sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)') + + expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') + end + end + + if Gitlab::Database.postgresql? + it 'replaces double quotes' do + sql = described_class.new('SELECT "x" FROM "y"') + + expect(sql.to_s).to eq('SELECT x FROM y') + end + end + end +end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a143fe4cfcd68a8f4de75b6ae85b393125025a9f --- /dev/null +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Gitlab::Metrics::RackMiddleware do + let(:app) { double(:app) } + + let(:middleware) { described_class.new(app) } + + let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } } + + describe '#call' do + before do + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) + end + + it 'tracks a transaction' do + expect(app).to receive(:call).with(env).and_return('yay') + + expect(middleware.call(env)).to eq('yay') + end + + it 'tags a transaction with the name and action of a controller' do + klass = double(:klass, name: 'TestController') + controller = double(:controller, class: klass, action_name: 'show') + + env['action_controller.instance'] = controller + + allow(app).to receive(:call).with(env) + + expect(middleware).to receive(:tag_controller). + with(an_instance_of(Gitlab::Metrics::Transaction), env) + + middleware.call(env) + end + end + + describe '#transaction_from_env' do + let(:transaction) { middleware.transaction_from_env(env) } + + it 'returns a Transaction' do + expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction) + end + + it 'tags the transaction with the request method and URI' do + expect(transaction.tags[:request_method]).to eq('GET') + expect(transaction.tags[:request_uri]).to eq('/foo') + end + end + + describe '#tag_controller' do + let(:transaction) { middleware.transaction_from_env(env) } + + it 'tags a transaction with the name and action of a controller' do + klass = double(:klass, name: 'TestController') + controller = double(:controller, class: klass, action_name: 'show') + + env['action_controller.instance'] = controller + + middleware.tag_controller(transaction, env) + + expect(transaction.tags[:action]).to eq('TestController#show') + end + end +end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..51a941c48cd7ea01385901f3f099836e154d0d27 --- /dev/null +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Sampler do + let(:sampler) { described_class.new(5) } + + after do + Allocations.stop if Gitlab::Metrics.mri? + end + + describe '#start' do + it 'gathers a sample at a given interval' do + expect(sampler).to receive(:sleep).with(5) + expect(sampler).to receive(:sample) + expect(sampler).to receive(:loop).and_yield + + sampler.start.join + end + end + + describe '#sample' do + it 'samples various statistics' do + expect(sampler).to receive(:sample_memory_usage) + expect(sampler).to receive(:sample_file_descriptors) + expect(sampler).to receive(:sample_objects) + expect(sampler).to receive(:sample_gc) + expect(sampler).to receive(:flush) + + sampler.sample + end + + it 'clears any GC profiles' do + expect(sampler).to receive(:flush) + expect(GC::Profiler).to receive(:clear) + + sampler.sample + end + end + + describe '#flush' do + it 'schedules the metrics using Sidekiq' do + expect(Gitlab::Metrics).to receive(:submit_metrics). + with([an_instance_of(Hash)]) + + sampler.sample_memory_usage + sampler.flush + end + end + + describe '#sample_memory_usage' do + it 'adds a metric containing the memory usage' do + expect(Gitlab::Metrics::System).to receive(:memory_usage). + and_return(9000) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('memory_usage', value: 9000). + and_call_original + + sampler.sample_memory_usage + end + end + + describe '#sample_file_descriptors' do + it 'adds a metric containing the amount of open file descriptors' do + expect(Gitlab::Metrics::System).to receive(:file_descriptor_count). + and_return(4) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('file_descriptors', value: 4). + and_call_original + + sampler.sample_file_descriptors + end + end + + describe '#sample_objects' do + it 'adds a metric containing the amount of allocated objects' do + expect(Gitlab::Metrics::Metric).to receive(:new). + with('object_counts', an_instance_of(Hash), an_instance_of(Hash)). + at_least(:once). + and_call_original + + sampler.sample_objects + end + end + + describe '#sample_gc' do + it 'adds a metric containing garbage collection statistics' do + expect(GC::Profiler).to receive(:total_time).and_return(0.24) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('gc_statistics', an_instance_of(Hash)). + and_call_original + + sampler.sample_gc + end + end +end diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5882e7d81c7c55059a6d71836ae937c6ecbeaede --- /dev/null +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Metrics::SidekiqMiddleware do + let(:middleware) { described_class.new } + + describe '#call' do + it 'tracks the transaction' do + worker = Class.new.new + + expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) + + middleware.call(worker, 'test', :test) { nil } + end + end + + describe '#tag_worker' do + it 'adds the worker class and action to the transaction' do + trans = Gitlab::Metrics::Transaction.new + worker = double(:worker, class: double(:class, name: 'TestWorker')) + + expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform') + + middleware.tag_worker(trans, worker) + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c6cd584663f26344861d6a20897e5a4f7b3c5dfe --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Subscribers::ActionView do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + let(:subscriber) { described_class.new } + + let(:event) do + root = Rails.root.to_s + + double(:event, duration: 2.1, + payload: { identifier: "#{root}/app/views/x.html.haml" }) + end + + before do + allow(subscriber).to receive(:current_transaction).and_return(transaction) + + allow(Gitlab::Metrics).to receive(:last_relative_application_frame). + and_return(['app/views/x.html.haml', 4]) + end + + describe '#render_template' do + it 'tracks rendering of a template' do + values = { duration: 2.1 } + tags = { + view: 'app/views/x.html.haml', + file: 'app/views/x.html.haml', + line: 4 + } + + expect(transaction).to receive(:add_metric). + with(described_class::SERIES, values, tags) + + subscriber.render_template(event) + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..05b6cc147162c76eb9533dd991aa4fa731be5f5c --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Subscribers::ActiveRecord do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + let(:subscriber) { described_class.new } + + let(:event) do + double(:event, duration: 0.2, + payload: { sql: 'SELECT * FROM users WHERE id = 10' }) + end + + before do + allow(subscriber).to receive(:current_transaction).and_return(transaction) + + allow(Gitlab::Metrics).to receive(:last_relative_application_frame). + and_return(['app/models/foo.rb', 4]) + end + + describe '#sql' do + it 'tracks the execution of a SQL query' do + sql = 'SELECT * FROM users WHERE id = ?' + values = { duration: 0.2 } + tags = { sql: sql, file: 'app/models/foo.rb', line: 4 } + + expect(transaction).to receive(:add_metric). + with(described_class::SERIES, values, tags) + + subscriber.sql(event) + end + end +end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f8c1d956ca1fb9e0c5cb41d2e7e2b6b7e217dfbd --- /dev/null +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::Metrics::System do + if File.exist?('/proc') + describe '.memory_usage' do + it "returns the process' memory usage in bytes" do + expect(described_class.memory_usage).to be > 0 + end + end + + describe '.file_descriptor_count' do + it 'returns the amount of open file descriptors' do + expect(described_class.file_descriptor_count).to be > 0 + end + end + else + describe '.memory_usage' do + it 'returns 0.0' do + expect(described_class.memory_usage).to eq(0.0) + end + end + + describe '.file_descriptor_count' do + it 'returns 0' do + expect(described_class.file_descriptor_count).to eq(0) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6862fc9e2d19d1939cca2b68478f4fda12f58512 --- /dev/null +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Transaction do + let(:transaction) { described_class.new } + + describe '#duration' do + it 'returns the duration of a transaction in seconds' do + transaction.run { sleep(0.5) } + + expect(transaction.duration).to be >= 0.5 + end + end + + describe '#run' do + it 'yields the supplied block' do + expect { |b| transaction.run(&b) }.to yield_control + end + + it 'stores the transaction in the current thread' do + transaction.run do + expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction) + end + end + + it 'removes the transaction from the current thread upon completion' do + transaction.run { } + + expect(Thread.current[described_class::THREAD_KEY]).to be_nil + end + end + + describe '#add_metric' do + it 'adds a metric tagged with the transaction UUID' do + expect(Gitlab::Metrics::Metric).to receive(:new). + with('foo', { number: 10 }, { transaction_id: transaction.uuid }) + + transaction.add_metric('foo', number: 10) + end + end + + describe '#add_tag' do + it 'adds a tag' do + transaction.add_tag(:foo, 'bar') + + expect(transaction.tags).to eq({ foo: 'bar' }) + end + end + + describe '#finish' do + it 'tracks the transaction details and submits them to Sidekiq' do + expect(transaction).to receive(:track_self) + expect(transaction).to receive(:submit) + + transaction.finish + end + end + + describe '#track_self' do + it 'adds a metric for the transaction itself' do + expect(transaction).to receive(:add_metric). + with(described_class::SERIES, { duration: transaction.duration }, {}) + + transaction.track_self + end + end + + describe '#submit' do + it 'submits the metrics to Sidekiq' do + transaction.track_self + + expect(Gitlab::Metrics).to receive(:submit_metrics). + with([an_instance_of(Hash)]) + + transaction.submit + end + end +end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c0682cac4dcdd439e547d4cf7f42caa7c16a9db --- /dev/null +++ b/spec/lib/gitlab/metrics_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::Metrics do + describe '.pool_size' do + it 'returns a Fixnum' do + expect(described_class.pool_size).to be_an_instance_of(Fixnum) + end + end + + describe '.timeout' do + it 'returns a Fixnum' do + expect(described_class.timeout).to be_an_instance_of(Fixnum) + end + end + + describe '.enabled?' do + it 'returns a boolean' do + expect([true, false].include?(described_class.enabled?)).to eq(true) + end + end + + describe '.hostname' do + it 'returns a String containing the hostname' do + expect(described_class.hostname).to eq(Socket.gethostname) + end + end + + describe '.last_relative_application_frame' do + it 'returns an Array containing a file path and line number' do + file, line = described_class.last_relative_application_frame + + expect(line).to eq(__LINE__ - 2) + expect(file).to eq('spec/lib/gitlab/metrics_spec.rb') + end + end + + describe '#submit_metrics' do + it 'prepares and writes the metrics to InfluxDB' do + connection = double(:connection) + pool = double(:pool) + + 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) + + described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }]) + end + end + + describe '#prepare_metrics' do + it 'returns a Hash with the keys as Symbols' do + metrics = described_class. + prepare_metrics([{ 'values' => {}, 'tags' => {} }]) + + expect(metrics).to eq([{ values: {}, tags: {} }]) + end + + it 'escapes tag values' do + metrics = described_class.prepare_metrics([ + { 'values' => {}, 'tags' => { 'foo' => 'bar=' } } + ]) + + expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }]) + end + + it 'drops empty tags' do + metrics = described_class.prepare_metrics([ + { 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } } + ]) + + expect(metrics).to eq([{ values: {}, tags: {} }]) + end + end + + describe '#escape_value' do + it 'escapes an equals sign' do + expect(described_class.escape_value('foo=')).to eq('foo\\=') + end + + it 'casts values to Strings' do + expect(described_class.escape_value(10)).to eq('10') + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 66dc5d4911d8cd064e455e03ff3099c1646b71d5..7d963795e17b48265d15c55ac92d4b4598716409 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -97,6 +97,16 @@ describe Gitlab::ReferenceExtractor, lib: true do expect(extracted.first.commit_to).to eq commit end + context 'with an external issue tracker' do + let(:project) { create(:jira_project) } + subject { described_class.new(project, project.creator) } + + it 'returns JIRA issues for a JIRA-integrated project' do + subject.analyze('JIRA-123 and FOOBAR-4567') + expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)] + end + end + context 'with a project with an underscore' do let(:other_project) { create(:project, path: 'test_project') } let(:issue) { create(:issue, project: other_project) } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 5f64453a35f29132faf5640052b25314fcdef9e4..35d8220ae548ebce18ae2547f72352eada10e5f8 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -27,6 +27,7 @@ # admin_notification_email :string(255) # shared_runners_enabled :boolean default(TRUE), not null # max_artifacts_size :integer default(100), not null +# runners_registration_token :string(255) # require 'spec_helper' diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 96b6f1dbca6b24e365a367bdef7acfc0e8cb03d4..1c22e3cb7c405363d30a575ce90178341f345da8 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -189,6 +189,12 @@ describe Ci::Build, models: true do it { is_expected.to eq(98.29) } end + + context 'using a regex capture' do + subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } + + it { is_expected.to eq(65) } + end end describe :variables do @@ -390,4 +396,68 @@ describe Ci::Build, models: true do it { is_expected.to include('gitlab-ci-token') } it { is_expected.to include(project.web_url[7..-1]) } end + + def create_mr(build, commit, factory: :merge_request, created_at: Time.now) + FactoryGirl.create(factory, + source_project_id: commit.gl_project_id, + target_project_id: commit.gl_project_id, + source_branch: build.ref, + created_at: created_at) + end + + describe :merge_request do + context 'when a MR has a reference to the commit' do + before do + @merge_request = create_mr(build, commit, factory: :merge_request) + + commits = [double(id: commit.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the single associated MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when there is not a MR referencing the commit' do + it 'returns nil' do + expect(build.merge_request).to be_nil + end + end + + context 'when more than one MR have a reference to the commit' do + before do + @merge_request = create_mr(build, commit, factory: :merge_request) + @merge_request.close! + @merge_request2 = create_mr(build, commit, factory: :merge_request) + + commits = [double(id: commit.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request2).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) + end + + it 'returns the first MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when a Build is created after the MR' do + before do + @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs) + commit2 = FactoryGirl.create :ci_commit, project: project + @build2 = FactoryGirl.create :ci_build, commit: commit2 + + commits = [double(id: commit.sha), double(id: commit2.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the current MR' do + expect(@build2.merge_request.id).to eq(@merge_request.id) + end + end + + end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index ac61c8fb525ff406e47b0bcd01a6a416e9a3d95f..b193e16e7f8dfee872048fe37dc65f62e537de2e 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -37,14 +37,14 @@ describe Ci::Commit, models: true do it 'returns ordered list of commits' do commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project + commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project expect(project.ci_commits.ordered).to eq([commit2, commit1]) end it 'returns commits ordered by committed_at and id, with nulls last' do commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project + commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1]) end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 0f13c4410cdc482943c7d9e17c25b804e81acb57..021d62cdf0ce324711e69333ece64ed84f2484c8 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -81,4 +81,36 @@ describe Issue, "Issuable" do expect(hook_data[:object_attributes]).to eq(issue.hook_attrs) end end + + describe '#card_attributes' do + it 'includes the author name' do + allow(issue).to receive(:author).and_return(double(name: 'Robert')) + allow(issue).to receive(:assignee).and_return(nil) + + expect(issue.card_attributes). + to eq({ 'Author' => 'Robert', 'Assignee' => nil }) + end + + it 'includes the assignee name' do + allow(issue).to receive(:author).and_return(double(name: 'Robert')) + allow(issue).to receive(:assignee).and_return(double(name: 'Douwe')) + + expect(issue.card_attributes). + to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) + end + end + + describe "votes" do + before do + author = create :user + project = create :empty_project + issue.notes.awards.create!(note: "thumbsup", author: author, project: project) + issue.notes.awards.create!(note: "thumbsdown", author: author, project: project) + end + + it "returns correct values" do + expect(issue.upvotes).to eq(1) + expect(issue.downvotes).to eq(1) + end + end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 6179882e93591f4556ddc9062b5c3ebba2dd14c0..20f0c561e44b16e3f9970f387ecc0a8d57e14fd8 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -1,5 +1,22 @@ require 'spec_helper' +describe Mentionable do + include Mentionable + + def author + nil + end + + describe :references do + let(:project) { create(:project) } + + it 'excludes JIRA references' do + allow(project).to receive_messages(jira_tracker?: true) + expect(referenced_mentionables(project, 'JIRA-123')).to be_empty + end + end +end + describe Issue, "Mentionable" do describe '#mentioned_users' do let!(:user) { create(:user, username: 'stranger') } diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index a9b0b64e5deccaa9663f8a701936de37133f70a1..30c0a04b84045b315094c1fb9a9b380de70f4a53 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' shared_examples 'TokenAuthenticatable' do describe 'dynamically defined methods' do - it { expect(described_class).to be_private_method_defined(:generate_token_for) } + it { expect(described_class).to be_private_method_defined(:generate_token) } + it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } @@ -24,11 +25,11 @@ describe ApplicationSetting, 'TokenAuthenticatable' do it_behaves_like 'TokenAuthenticatable' describe 'generating new token' do - subject { described_class.new } - let(:token) { subject.send(token_field) } - context 'token is not generated yet' do - it { expect(token).to be nil } + describe 'token field accessor' do + subject { described_class.new.send(token_field) } + it { is_expected.to_not be_blank } + end describe 'ensured token' do subject { described_class.new.send("ensure_#{token_field}") } @@ -36,11 +37,21 @@ describe ApplicationSetting, 'TokenAuthenticatable' do it { is_expected.to be_a String } it { is_expected.to_not be_blank } end + + describe 'ensured! token' do + subject { described_class.new.send("ensure_#{token_field}!") } + + it 'should persist new token' do + expect(subject).to eq described_class.current[token_field] + end + end end context 'token is generated' do before { subject.send("reset_#{token_field}!") } - it { expect(token).to be_a String } + it 'persists a new token 'do + expect(subject.send(:read_attribute, token_field)).to be_a String + end end end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index ba03e6aabd0352ecae60e036edd3e03b895b016a..197c99cd007ea9c80f30ef2474210b85d1d57907 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -62,4 +62,14 @@ describe GlobalMilestone, models: true do expect(@global_milestone.milestones.count).to eq(3) end end + + describe :safe_title do + let(:milestone) { create(:milestone, title: "git / test", project: project1) } + + it 'should strip out slashes and spaces' do + global_milestone = GlobalMilestone.new(milestone.title, [milestone]) + + expect(global_milestone.safe_title).to eq('git-test') + end + end end diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1634265b439d1c18e8fd5ae4f010aa91cdc83cf1 --- /dev/null +++ b/spec/models/jira_issue_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe JiraIssue do + let(:project) { create(:project) } + subject { JiraIssue.new('JIRA-123', project) } + + describe 'id' do + subject { super().id } + it { is_expected.to eq('JIRA-123') } + end + + describe 'iid' do + subject { super().iid } + it { is_expected.to eq('JIRA-123') } + end + + describe 'to_s' do + subject { super().to_s } + it { is_expected.to eq('JIRA-123') } + end + + describe :== do + specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) } + specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) } + + it 'only compares with JiraIssues' do + expect(subject).not_to eq('JIRA-123') + end + end +end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index d7fe01976d831a79bf760d9a23e5112a63848d6f..c962b83644a045664a6df0c86de8a401d0fb9588 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -81,7 +81,7 @@ describe Key, models: true do it 'rejects the multiple line key' do key = build(:key) - key.key.gsub!(' ', "\n") + key.key.tr!(' ', "\n") expect(key).not_to be_valid end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1aeba9b2b3bcef80dbdf80053ac632b34be003af..e0653a8327d61ac6de3dfdac0c8090bbbfb1ac2f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -164,6 +164,17 @@ describe MergeRequest, models: true do expect(subject.closes_issues).to include(issue2) end + + context 'for a project with JIRA integration' do + let(:issue0) { JiraIssue.new('JIRA-123', subject.project) } + let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) } + + it 'returns sorted JiraIssues' do + allow(subject.project).to receive_messages(default_branch: subject.target_branch) + + expect(subject.closes_issues).to eq([issue0, issue1]) + end + end end describe "#work_in_progress?" do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 216c7dabae03f910b17ff1e30c2f62aa0b47433b..593d8f76215de366247727af77df18a42926ff5e 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -137,9 +137,14 @@ describe Note, models: true do create :note, note: "smile", is_award: true end - it "returns grouped array of notes" do - expect(Note.grouped_awards.first.first).to eq("smile") - expect(Note.grouped_awards.first.last).to match_array(Note.all) + it "returns grouped hash of notes" do + expect(Note.grouped_awards.keys.size).to eq(3) + expect(Note.grouped_awards["smile"]).to match_array(Note.all) + end + + it "returns thumbsup and thumbsdown always" do + expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none) + expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none) end end @@ -164,8 +169,8 @@ describe Note, models: true do let(:issue) { create :issue } it "converts aliases to actual name" do - note = create :note, note: ":thumbsup:", noteable: issue - expect(note.reload.note).to eq("+1") + note = create :note, note: ":+1:", noteable: issue + expect(note.reload.note).to eq("thumbsup") end end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index a5662b08bdad255cec9634a944382f37ddd76251..91dd92b7c679aa06af80e1c27c365a27ac950039 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -57,23 +57,21 @@ describe HipchatService, models: true do it 'should use v1 if version is provided' do allow(hipchat).to receive(:api_version).and_return('v1') - expect(HipChat::Client).to receive(:new). - with(token, - api_version: 'v1', - server_url: server_url). - and_return( - double(:hipchat_service).as_null_object) + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v1', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) hipchat.execute(push_sample_data) end it 'should use v2 as the version when nothing is provided' do allow(hipchat).to receive(:api_version).and_return('') - expect(HipChat::Client).to receive(:new). - with(token, - api_version: 'v2', - server_url: server_url). - and_return( - double(:hipchat_service).as_null_object) + expect(HipChat::Client).to receive(:new).with( + token, + api_version: 'v2', + server_url: server_url + ).and_return(double(:hipchat_service).as_null_object) hipchat.execute(push_sample_data) end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 7d91ebe9ce652cd3f28befb0e0623271990f99fd..2f8193170aedf3009a9377879a2b4da0c7c11c08 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -26,6 +26,113 @@ describe JiraService, models: true do it { is_expected.to have_one :service_hook } end + describe "Execute" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request) } + + before do + @jira_service = JiraService.new + allow(@jira_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + project_url: 'http://jira.example.com', + username: 'gitlab_jira_username', + password: 'gitlab_jira_password' + ) + @jira_service.save # will build API URL, as api_url was not specified above + @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) + # https://github.com/bblimke/webmock#request-with-basic-authentication + @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' + @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' + + WebMock.stub_request(:post, @api_url) + WebMock.stub_request(:post, @comment_url) + end + + it "should call JIRA API" do + @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project)) + expect(WebMock).to have_requested(:post, @comment_url).with( + body: /Issue solved with/ + ).once + end + + it "calls the api with jira_issue_transition_id" do + @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' + @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project)) + expect(WebMock).to have_requested(:post, @api_url).with( + body: /this-is-a-custom-id/ + ).once + end + end + + describe "Stored password invalidation" do + let(:project) { create(:project) } + + context "when a password was previously set" do + before do + @jira_service = JiraService.create( + project: create(:project), + properties: { + api_url: 'http://jira.example.com/rest/api/2', + username: 'mic', + password: "password" + } + ) + end + + it "reset password if url changed" do + @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.save + expect(@jira_service.password).to be_nil + end + + it "does not reset password if username changed" do + @jira_service.username = "some_name" + @jira_service.save + expect(@jira_service.password).to eq("password") + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.password = 'password' + @jira_service.save + expect(@jira_service.password).to eq("password") + expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + end + + it "should reset password if url changed, even if setter called multiple times" do + @jira_service.api_url = 'http://jira1.example.com/rest/api/2' + @jira_service.api_url = 'http://jira1.example.com/rest/api/2' + @jira_service.save + expect(@jira_service.password).to be_nil + end + end + + context "when no password was previously set" do + before do + @jira_service = JiraService.create( + project: create(:project), + properties: { + api_url: 'http://jira.example.com/rest/api/2', + username: 'mic' + } + ) + end + + it "saves password if new url is set together with password" do + @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.password = 'password' + @jira_service.save + expect(@jira_service.password).to eq("password") + expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + end + + end + end + + describe "Validations" do context "active" do before do @@ -78,11 +185,12 @@ describe JiraService, models: true do context 'when gitlab.yml was initialized' do before do - settings = { "jira" => { - "title" => "Jira", - "project_url" => "http://jira.sample/projects/project_a", - "issues_url" => "http://jira.sample/issues/:id", - "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" + settings = { + "jira" => { + "title" => "Jira", + "project_url" => "http://jira.sample/projects/project_a", + "issues_url" => "http://jira.sample/issues/:id", + "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" } } allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index ebf8837570e1fa00870ccfb32bfbcaf554fccaa3..06006b9a4f530d525f28217f30be90550b64a4e1 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -89,10 +89,10 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq( - "Test User commented on " \ - "<url|issue #20> in <somewhere.com|project_name>: " \ - "*issue title*") - expected_attachments = [ + "Test User commented on " \ + "<url|issue #20> in <somewhere.com|project_name>: " \ + "*issue title*") + expected_attachments = [ { text: "comment on an issue", color: color, diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 87582e074948b766cbaaea53c804bcf8d2a167bb..400bdf2d962bcdf35ded86db2715f44aba542fa4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -172,13 +172,17 @@ describe Project, models: true do describe '#get_issue' do let(:project) { create(:empty_project) } - let(:issue) { create(:issue, project: project) } + let!(:issue) { create(:issue, project: project) } context 'with default issues tracker' do it 'returns an issue' do expect(project.get_issue(issue.iid)).to eq issue end + it 'returns count of open issues' do + expect(project.open_issues_count).to eq(1) + end + it 'returns nil when no issue found' do expect(project.get_issue(999)).to be_nil end @@ -548,4 +552,28 @@ describe Project, models: true do end end end + + describe '#visibility_level_allowed?' do + let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + + context 'when checking on non-forked project' do + it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } + it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy } + it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_truthy } + end + + context 'when checking on forked project' do + let(:forked_project) { create :forked_project_with_submodules } + + before do + forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id) + forked_project.save + end + + it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } + it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy } + it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey } + end + + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index daa9d1087bfad6e47d5aebb4e3fa13f4168cd82d..2f184bbaf92c50fffd79529ea173d84a89d50421 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null @@ -462,8 +463,8 @@ describe User, models: true do expect(User.search(user1.username.downcase).to_a).to eq([user1]) expect(User.search(user2.username.upcase).to_a).to eq([user2]) expect(User.search(user2.username.downcase).to_a).to eq([user2]) - expect(User.search(user1.username.downcase).to_a.count).to eq(2) - expect(User.search(user2.username.downcase).to_a.count).to eq(1) + expect(User.search(user1.username.downcase).to_a.size).to eq(2) + expect(User.search(user2.username.downcase).to_a.size).to eq(1) end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 5c1b58535cc3b636122791e49e27a0e309b3950a..36461e84c3ab7c33283415c82505036c35d1e0d1 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -118,7 +118,7 @@ describe API::API, api: true do branch_name: 'new design', ref: branch_sha expect(response.status).to eq(400) - expect(json_response['message']).to eq('Branch name invalid') + expect(json_response['message']).to eq('Branch name is invalid') end it 'should return 400 if branch already exists' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a91fa7353213d958ef8bf85bb0380464dab23c10..e194eb93cf48757cd7b21ee53080be87ab74a8c3 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -6,7 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e784b7d1f2d9c96edbf47bde6fe1085f661213b2..7f0f9454b1006a193e5f10470e50857973af4a4b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -65,6 +65,22 @@ describe API::API, api: true do expect(json_response.first.keys).to include('tag_list') end + it 'should include open_issues_count' do + get api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('open_issues_count') + end + + it 'should not include open_issues_count' do + project.update_attributes( { issues_enabled: false } ) + + get api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).not_to include('open_issues_count') + end + context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } @@ -115,6 +131,7 @@ describe API::API, api: true do expect(json_response).to satisfy do |response| response.one? do |entry| + entry.has_key?('permissions') && entry['name'] == project.name && entry['owner']['username'] == user.username end @@ -123,6 +140,25 @@ describe API::API, api: true do end end + describe 'GET /projects/starred' do + before do + admin.starred_projects << project + admin.save! + end + + it 'should return the starred projects' do + get api('/projects/all', admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + + expect(json_response).to satisfy do |response| + response.one? do |entry| + entry['name'] == project.name + end + end + end + end + describe 'POST /projects' do context 'maximum number of projects reached' do it 'should not create new project and respond with 403' do @@ -347,6 +383,18 @@ describe API::API, api: true do end describe 'permissions' do + context 'all projects' do + it 'Contains permission information' do + project.team << [user, :master] + get api("/projects", user) + + expect(response.status).to eq(200) + expect(json_response.first['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response.first['permissions']['group_access']).to be_nil + end + end + context 'personal project' do it 'Sets project access and returns 200' do project.team << [user, :master] @@ -455,7 +503,7 @@ describe API::API, api: true do end end - describe 'PUT /projects/:id/snippets/:shippet_id' do + describe 'PUT /projects/:id/snippets/:snippet_id' do it 'should update an existing project snippet' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), code: 'updated code' diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index b180d2fec77b8fd52197589b5e392c90bd1bf6ba..fed9ae1949b419834f3f6d1593765e449f27ff3f 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -29,7 +29,7 @@ describe API::API, api: true do if required_attributes.empty? expected_code = 200 else - attrs.delete(required_attributes.shuffle.first) + attrs.delete(required_attributes.sample) expected_code = 400 end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2f609c63330d9d1142efbea168ccfd2de48cd61f..4f278551d0741b8cf7001fbf2f39570c2ef5a5fb 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -27,6 +27,13 @@ describe API::API, api: true do user['username'] == username end['username']).to eq(username) end + + it "should return one user" do + get api("/users?username=#{omniauth_user.username}", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['username']).to eq(omniauth_user.username) + end end context "when admin" do diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 567da013e6f22ac3a6872456c8b51714fe17ebc5..5942aa7a1b59f27cade4f9b0fe08018bd0a76ea4 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -8,7 +8,6 @@ describe Ci::API::API do before do stub_gitlab_calls - stub_application_setting(ensure_runners_registration_token: registration_token) stub_application_setting(runners_registration_token: registration_token) end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index 798c480b81af73a159eeeb0da3edc5cd836d710e..ea5dcfa068a0ffe114ebc1af7d3c5251b53da1ad 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -17,7 +17,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) end it { expect(commit).to be_kind_of(Ci::Commit) } @@ -34,7 +34,7 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) expect(result).to be_persisted end @@ -47,26 +47,24 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: [{ message: "Message" }] - ) + ) expect(result).to be_persisted end end - it 'skips commits without .gitlab-ci.yml' do + it 'skips creating ci_commit for refs without .gitlab-ci.yml' do stub_ci_commit_yaml_file(nil) result = service.execute(project, user, ref: 'refs/heads/0_1', before: '00000000', after: '31das312', commits: [{ message: 'Message' }] - ) - expect(result).to be_persisted - expect(result.builds.any?).to be_falsey - expect(result.status).to eq('skipped') - expect(result.yaml_errors).to be_nil + ) + expect(result).to be_falsey + expect(Ci::Commit.count).to eq(0) end - it 'skips commits if yaml is invalid' do + it 'fails commits if yaml is invalid' do message = 'message' allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } stub_ci_commit_yaml_file('invalid: file: file') @@ -76,7 +74,8 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.any?).to be false expect(commit.status).to eq('failed') expect(commit.yaml_errors).to_not be_nil @@ -96,7 +95,8 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") end @@ -110,8 +110,9 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.first.name).to eq("staging") end @@ -123,7 +124,8 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") expect(commit.yaml_errors).to be_nil @@ -139,7 +141,8 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.count(:all)).to eq(2) commit = service.execute(project, user, @@ -147,7 +150,8 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.builds.count(:all)).to eq(2) end @@ -161,8 +165,9 @@ describe CreateCommitBuildsService, services: true do before: '00000000', after: '31das312', commits: commits - ) + ) + expect(commit).to be_persisted expect(commit.status).to eq("failed") expect(commit.builds.any?).to be false end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index a04c242cf0e72a26069c7f4735bf74b80e81018b..c1080ef190aa2303641d110aa0d45a99b8fa805d 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -265,6 +265,75 @@ describe GitPushService, services: true do expect(Issue.find(issue.id)).to be_opened end end + + # EE-only tests + context "for jira issue tracker" do + include JiraServiceHelper + + let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? } + + before do + jira_service_settings + + WebMock.stub_request(:post, jira_api_transition_url) + WebMock.stub_request(:post, jira_api_comment_url) + WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) + WebMock.stub_request(:get, jira_api_test_url) + + allow(closing_commit).to receive_messages({ + issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), + safe_message: message, + author_name: commit_author.name, + author_email: commit_author.email + }) + + allow(project.repository).to receive_messages(commits_between: [closing_commit]) + end + + after do + jira_tracker.destroy! + end + + context "mentioning an issue" do + let(:message) { "this is some work.\n\nrelated to JIRA-1" } + + it "should initiate one api call to jira server to mention the issue" do + service.execute(project, user, @oldrev, @newrev, @ref) + + expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + body: /mentioned this issue in/ + ).once + end + end + + context "closing an issue" do + let(:message) { "this is some work.\n\ncloses JIRA-1" } + + it "should initiate one api call to jira server to close the issue" do + transition_body = { + transition: { + id: '2' + } + }.to_json + + service.execute(project, user, @oldrev, @newrev, @ref) + expect(WebMock).to have_requested(:post, jira_api_transition_url).with( + body: transition_body + ).once + end + + it "should initiate one api call to jira server to comment on the issue" do + comment_body = { + body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." + }.to_json + + service.execute(project, user, @oldrev, @newrev, @ref) + expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + body: comment_body + ).once + end + end + end end describe "empty project" do diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index e2d15f1a83d7d93ce3abba2e3225471136a8371d..b982274c529073ee106ed318bf56d49d9c68c9d4 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -58,14 +58,14 @@ describe GitTagPushService, services: true do it { is_expected.to include(timestamp: @commit.date.xmlschema) } it do is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - @commit.id - ].join('/') - ) + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) end context "with a author" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d7a898e85ff6f39e8c468daf1085f63aa0c77ccd..c103752198d7f21c6cb4b1967c9386b5aa6aa906 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -115,6 +115,7 @@ describe NotificationService, services: true do before do build_team(note.project) + note.project.team << [note.author, :master] ActionMailer::Base.deliveries.clear end @@ -126,6 +127,8 @@ describe NotificationService, services: true do note.project.team.members.each do |member| # User with disabled notification should not be notified next if member.id == @u_disabled.id + # Author should not be notified + next if member.id == note.author.id should_email(member) end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index c36d45819896a9d78f0bc0911c7e09deb721cf1e..3c06a8901634512a99828a8bfab6144fcfb56a1b 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -100,6 +100,45 @@ describe Projects::UpdateService, services: true do end end + describe :visibility_level do + let(:user) { create :user, admin: true } + let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:opts) { {} } + + before do + forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id) + forked_project.save + + @created_internal = project.internal? + @fork_created_internal = forked_project.internal? + end + + context 'should update forks visibility level when parent set to more restrictive' do + before do + opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + update_project(project, user, opts).inspect + end + + it { expect(@created_internal).to be_truthy } + it { expect(@fork_created_internal).to be_truthy } + it { expect(project.private?).to be_truthy } + it { expect(project.forks.first.private?).to be_truthy } + end + + context 'should not update forks visibility level when parent set to less restrictive' do + before do + opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + update_project(project, user, opts).inspect + end + + it { expect(@created_internal).to be_truthy } + it { expect(@fork_created_internal).to be_truthy } + it { expect(project.public?).to be_truthy } + it { expect(project.forks.first.internal?).to be_truthy } + end + end + def update_project(project, user, opts) Projects::UpdateService.new(project, user, opts).execute end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0a4f9b230e890d6b48a528b7a931b01b6853c630..c9f828ae2f7a5e99f63a889e269885b5180e3bef 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -425,4 +425,65 @@ describe SystemNoteService, services: true do end end end + + include JiraServiceHelper + + describe 'JIRA integration' do + let(:project) { create(:project) } + let(:author) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } + let(:jira_issue) { JiraIssue.new("JIRA-1", project)} + let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? } + let(:commit) { project.commit } + + context 'in JIRA issue tracker' do + before do + jira_service_settings + WebMock.stub_request(:post, jira_api_comment_url) + end + + after do + jira_tracker.destroy! + end + + describe "new reference" do + before do + WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) + end + + subject { described_class.cross_reference(jira_issue, commit, author) } + + it { is_expected.to eq(jira_status_message) } + end + + describe "existing reference" do + before do + message = "[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]." + WebMock.stub_request(:get, jira_api_comment_url).to_return(body: "{\"comments\":[{\"body\":\"#{message}\"}]}") + end + + subject { described_class.cross_reference(jira_issue, commit, author) } + it { is_expected.not_to eq(jira_status_message) } + end + end + + context 'issue from an issue' do + context 'in JIRA issue tracker' do + before do + jira_service_settings + WebMock.stub_request(:post, jira_api_comment_url) + WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) + end + + after do + jira_tracker.destroy! + end + + subject { described_class.cross_reference(jira_issue, issue, author) } + + it { is_expected.to eq(jira_status_message) } + end + end + end end diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 124bb76e678df6760b463830d3673601cb91e5c9..48d114896d0ff5f8a33c21390053e0d43499f697 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -42,7 +42,7 @@ describe UpdateSnippetService, services: true do CreateSnippetService.new(project, user, opts).execute end - def update_snippet(project = nil, user, snippet, opts) + def update_snippet(project, user, snippet, opts) UpdateSnippetService.new(project, user, snippet, opts).execute end end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3f496359b1675f45aebf946ce24d8b1a7818f87 --- /dev/null +++ b/spec/support/jira_service_helper.rb @@ -0,0 +1,67 @@ +module JiraServiceHelper + + def jira_service_settings + properties = { + "title"=>"JIRA tracker", + "project_url"=>"http://jira.example/issues/?jql=project=A", + "issues_url"=>"http://jira.example/browse/JIRA-1", + "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa", + "api_url"=>"http://jira.example/rest/api/2" + } + + jira_tracker.update_attributes(properties: properties, active: true) + end + + def jira_status_message + "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}." + end + + def jira_issue_comments + "{\"startAt\":0,\"maxResults\":11,\"total\":11, + \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\", + \"id\":\"10609\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\", + \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\", + \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\", + \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\", + \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\", + \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"}, + \"displayName\":\"GitLab\",\"active\":true}, + \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned JIRA-1 in Merge request of [gitlab-org/gitlab-test|http://localhost:3000/gitlab-org/gitlab-test/merge_requests/2].\", + \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\", + \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\", + \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\", + \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\", + \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true}, + \"created\":\"2015-02-12T22:47:07.826+0100\", + \"updated\":\"2015-02-12T22:47:07.826+0100\"}, + {\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10700\", + \"id\":\"10700\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\", + \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\", + \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\", + \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\", + \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\", + \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true}, + \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned this issue in [a commit of h5bp/html5-boilerplate|http://localhost:3000/h5bp/html5-boilerplate/commit/2439f77897122fbeee3bfd9bb692d3608848433e].\", + \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\", + \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\", + \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\", + \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\", + \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true}, + \"created\":\"2015-04-01T03:45:55.667+0200\", + \"updated\":\"2015-04-01T03:45:55.667+0200\" + } + ]}" + end + + def jira_api_comment_url + 'http://jira.example/rest/api/2/issue/JIRA-1/comment' + end + + def jira_api_transition_url + 'http://jira.example/rest/api/2/issue/JIRA-1/transitions' + end + + def jira_api_test_url + 'http://jira.example/rest/api/2/myself' + end +end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index aadf791bf3ff1ee5c245dc11e31dd1ac1d6f8d25..aa8258d6dad6ef0904684dd8a3869da479010ae4 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -45,12 +45,12 @@ eos def another_sample_commit OpenStruct.new( - id: "e56497bb5f03a90a51293fc6d516788730953899", - parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', - author_full_name: "Sytse Sijbrandij", - author_email: "sytse@gitlab.com", - files_changed_count: 1, - message: <<eos + id: "e56497bb5f03a90a51293fc6d516788730953899", + parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', + author_full_name: "Sytse Sijbrandij", + author_email: "sytse@gitlab.com", + files_changed_count: 1, + message: <<eos Add directory structure for tree_helper spec This directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 245f066df1f6a016baa738e15e3c1642133e9a6f..dae31992620a0b86de8f8a64352b65cb4c669e0b 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -9,20 +9,22 @@ describe RepositoryForkWorker do describe "#perform" do it "creates a new repository from a fork" do expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).with( - project.path_with_namespace, - fork_project.namespace.path). - and_return(true) + project.path_with_namespace, + fork_project.namespace.path + ).and_return(true) - subject.perform(project.id, - project.path_with_namespace, - fork_project.namespace.path) + subject.perform( + project.id, + project.path_with_namespace, + fork_project.namespace.path) end it "handles bad fork" do expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false) - subject.perform(project.id, - project.path_with_namespace, - fork_project.namespace.path) + subject.perform( + project.id, + project.path_with_namespace, + fork_project.namespace.path) end end end diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb index f9d87d97014f94e9e791882bbc7dfe6b5a875f66..665ec20f2243a7f5cc68fa72bd244d341bad8ccd 100644 --- a/spec/workers/stuck_ci_builds_worker_spec.rb +++ b/spec/workers/stuck_ci_builds_worker_spec.rb @@ -15,7 +15,7 @@ describe StuckCiBuildsWorker do end it 'gets dropped if it was updated over 2 days ago' do - build.update!(updated_at: 2.day.ago) + build.update!(updated_at: 2.days.ago) StuckCiBuildsWorker.new.perform is_expected.to eq('failed') end @@ -35,7 +35,7 @@ describe StuckCiBuildsWorker do end it "is still #{status}" do - build.update!(updated_at: 2.day.ago) + build.update!(updated_at: 2.days.ago) StuckCiBuildsWorker.new.perform is_expected.to eq(status) end