diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 53f115c92c..1dc49ca336 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,6 @@ image: "ruby:2.1" services: - mysql:latest - - postgres:latest - redis:latest cache: @@ -35,134 +34,86 @@ spec:feature: script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - tags: - - ruby - - mysql spec:api: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - tags: - - ruby - - mysql spec:models: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - tags: - - ruby - - mysql spec:lib: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - tags: - - ruby - - mysql spec:services: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - tags: - - ruby - - mysql spec:other: stage: test script: - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - tags: - - ruby - - mysql spinach:project:half: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - tags: - - ruby - - mysql spinach:project:rest: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - tags: - - ruby - - mysql spinach:other: stage: test script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - tags: - - ruby - - mysql teaspoon: stage: test script: - RAILS_ENV=test bundle exec teaspoon - tags: - - ruby - - mysql rubocop: stage: test script: - bundle exec rubocop - tags: - - ruby - - mysql scss-lint: stage: test script: - bundle exec rake scss_lint - tags: - - ruby brakeman: stage: test script: - bundle exec rake brakeman - tags: - - ruby - - mysql flog: stage: test script: - bundle exec rake flog - tags: - - ruby - - mysql flay: stage: test script: - bundle exec rake flay - tags: - - ruby - - mysql bundler:audit: stage: test only: - master script: - - "bundle exec bundle-audit update" - - "bundle exec bundle-audit check --ignore OSVDB-115941" - tags: - - ruby - - mysql + - "bundle exec bundle-audit check --update --ignore OSVDB-115941" # Ruby 2.2 jobs @@ -178,9 +129,6 @@ spec:feature:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:api:ruby22: stage: test @@ -193,9 +141,6 @@ spec:api:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:models:ruby22: stage: test @@ -208,9 +153,6 @@ spec:models:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:lib:ruby22: stage: test @@ -223,9 +165,6 @@ spec:lib:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:services:ruby22: stage: test @@ -238,9 +177,6 @@ spec:services:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spec:other:ruby22: stage: test @@ -253,9 +189,6 @@ spec:other:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:project:half:ruby22: stage: test @@ -269,9 +202,6 @@ spinach:project:half:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:project:rest:ruby22: stage: test @@ -285,9 +215,6 @@ spinach:project:rest:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql spinach:other:ruby22: stage: test @@ -301,10 +228,6 @@ spinach:other:ruby22: key: "ruby22" paths: - vendor - tags: - - ruby - - mysql - notify:slack: stage: notifications diff --git a/.rubocop.yml b/.rubocop.yml index 71273ce609..2fda0b0311 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -691,7 +691,7 @@ Style/ZeroLengthPredicate: # branches, and conditions. Metrics/AbcSize: Enabled: true - Max: 70 + Max: 60 # Avoid excessive block nesting. Metrics/BlockNesting: diff --git a/.scss-lint.yml b/.scss-lint.yml index 937d3407b6..835a4a88c4 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -7,21 +7,44 @@ exclude: - 'app/assets/stylesheets/pages/emojis.scss' linters: + # Reports when you use improper spacing around ! (the "bang") in !default, + # !global, !important, and !optional flags. BangFormat: enabled: false + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false + # Reports when you define a rule set using a selector with chained classes + # (a.k.a. adjoining classes). + ChainedClasses: + enabled: false + + # Prefer hexadecimal color codes over color keywords. + # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false + # Prefer color literals (keywords or hexadecimal codes) to be used only in + # variable declarations. They should be referred to via variables everywhere + # else. ColorVariable: enabled: false + # Which form of comments to prefer in CSS. Comment: enabled: false + + # Reports @debug statements (which you probably left behind accidentally). + DebugStatement: + enabled: false + # Rule sets should be ordered as follows: + # - @extend declarations + # - @include declarations without inner @content + # - properties, @include declarations with inner @content + # - nested rule sets. DeclarationOrder: enabled: false @@ -32,15 +55,25 @@ linters: DisableLinterReason: enabled: true + # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: false + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: false + # Reports when you have an empty rule set. EmptyRule: enabled: false + # Reports when you have an @extend directive. + ExtendDirective: + enabled: false + + # Files should always have a final newline. This results in better diffs + # when adding lines to the file, since SCM systems such as git won't + # think that you touched the last line. FinalNewline: enabled: false @@ -53,12 +86,17 @@ linters: HexNotation: enabled: true + # Avoid using ID selectors. IdSelector: enabled: false + # The basenames of @imported SCSS partials should not begin with an + # underscore and should not include the filename extension. ImportPath: enabled: false + # Avoid using !important in properties. It is usually indicative of a + # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false @@ -67,33 +105,51 @@ linters: enabled: true width: 2 + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false + # Functions, mixins, variables, and placeholders should be declared + # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false + # Avoid nesting selectors too deeply. NestingDepth: enabled: false + # Always use placeholder selectors in @extend. PlaceholderInExtend: enabled: false + # Sort properties in a strict order. PropertySortOrder: enabled: false + # Reports when you use an unknown or disabled CSS property + # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false + # Configure which units are allowed for property values. + PropertyUnits: + enabled: false + + # Pseudo-elements, like ::before, and ::first-letter, should be declared + # with two colons. Pseudo-classes, like :hover and :first-child, should + # be declared with one colon. PseudoElement: enabled: false + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false @@ -113,9 +169,12 @@ linters: enabled: true allow_single_line_rule_sets: true + # Split selectors onto separate lines after each comma, and have each + # individual selector occupy a single line. SingleLinePerSelector: enabled: false + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false @@ -128,28 +187,75 @@ linters: # colon. SpaceAfterPropertyName: enabled: true + + # Variables should be formatted with a single space separating the colon + # from the variable's value. + SpaceAfterVariableColon: + enabled: false + + # Variables should be formatted with no space between the name and the + # colon. + SpaceAfterVariableName: + enabled: false + # Operators should be formatted with a single space on both sides of an + # infix operator. SpaceAroundOperator: enabled: false + # Opening braces should be preceded by a single space. SpaceBeforeBrace: + enabled: true + + # Parentheses should not be padded with spaces. + SpaceBetweenParens: enabled: false + # Enforces that string literals should be written with a consistent form + # of quotes (single or double). StringQuotes: enabled: false + # Property values, @extend, @include, and @import directives, and variable + # declarations should always end with a semicolon. TrailingSemicolon: enabled: false + # Reports lines containing trailing whitespace. TrailingWhitespace: enabled: false + # Don't write trailing zeros for numeric values with a decimal point. + TrailingZero: + enabled: false + + # Don't use the `all` keyword to specify transition properties. + TransitionAll: + enabled: false + + # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: enabled: false + # Do not use parent selector references (&) when they would otherwise + # be unnecessary. UnnecessaryParentReference: enabled: false + + # URLs should be valid and not contain protocols or domain names. + UrlFormat: + enabled: false + # URLs should always be enclosed within quotes. + UrlQuotes: + enabled: false + + # Properties, like color and font, are easier to read and maintain + # when defined using variables rather than literals. + VariableForProperty: + enabled: false + + # Avoid vendor prefixes. Or rather: don't write them yourself. VendorPrefix: enabled: false diff --git a/CHANGELOG b/CHANGELOG index 4652ebb113..382318a203 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,12 +1,113 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) + - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) + - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) + - All images in discussions and wikis now link to their source files !3464 (Connor Shea). + - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) + - Improved Markdown rendering performance !3389 (Yorick Peterse) + - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) + - Expose project badges in project settings + - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) + - Expose label description in API (Mariusz Jachimowicz) + - Allow back dating on issues when created through the API + - Fix Error 500 after renaming a project path (Stan Hu) + - Fix avatar stretching by providing a cropping feature + - API: Expose `subscribed` for issues and merge requests (Robert Schilling) + - Allow SAML to handle external users based on user's information !3530 + - Add endpoints to archive or unarchive a project !3372 + - Add links to CI setup documentation from project settings and builds pages + - Handle nil descriptions in Slack issue messages (Stan Hu) + - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) + - Add default scope to projects to exclude projects pending deletion + - Ensure empty recipients are rejected in BuildsEmailService + - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) + - API: Fix milestone filtering by `iid` (Robert Schilling) + - API: Delete notes of issues, snippets, and merge requests (Robert Schilling) + - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) + - Better errors handling when creating milestones inside groups + - Hide `Create a group` help block when creating a new project in a group + - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) + - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Fix creation of merge requests for orphaned branches (Stan Hu) + - API: Ability to retrieve a single tag (Robert Schilling) + - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) + - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) + - Fix admin/projects when using visibility levels on search (PotHix) + - Build status notifications + - API: Expose user location (Robert Schilling) + - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 + - Update number of Todos in the sidebar when it's marked as "Done". !3600 + - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) + - API: User can leave a project through the API when not master or owner. !3613 -v 8.6.1 (unreleased) - - Do not allow to move issue if it has not been persisted - - Fix an issue causing the Dashboard/Milestones page to be blank - - Fix build dependencies, when the dependency is a string +v 8.6.6 + - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) + +v 8.6.5 + - Fix importing from GitHub Enterprise. !3529 + - Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533 + - Check permissions when user attempts to import members from another project. !3535 + - Only update repository language if it is not set to improve performance. !3556 + - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu). !3583 + - Unblock user when active_directory is disabled and it can be found !3550 + - Fix a 2FA authentication spoofing vulnerability. + +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) + - Redesign the Labels page + +v 8.6.3 + - Mentions on confidential issues doesn't create todos for non-members. !3374 + - Destroy related todos when an Issue/MR is deleted. !3376 + - Fix error 500 when target is nil on todo list. !3376 + - Fix copying uploads when moving issue to another project. !3382 + - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432 + - Fix raw/rendered diff producing different results on merge requests. !3450 + - Fix commit comment alignment (Stan Hu). !3466 + - Fix Error 500 when searching for a comment in a project snippet. !3468 + - Allow temporary email as notification email. !3477 + - Fix issue with dropdowns not selecting values. !3478 + - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280 + +v 8.6.2 + - Fix dropdown alignment. !3298 + - Fix issuable sidebar overlaps on tablet. !3299 + - Make dropdowns pixel perfect. !3337 + - Fix order of steps to prevent PostgreSQL errors when running migration. !3355 + - Fix bold text in issuable sidebar. !3358 + - Fix error with anonymous token in applications settings. !3362 + - Fix the milestone 'upcoming' filter. !3364 + !3368 + - Fix comments on confidential issues showing up in activity feed to non-members. !3375 + - Fix `NoMethodError` when visiting CI root path at `/ci`. !3377 + - Add a tooltip to new branch button in issue page. !3380 + - Fix an issue hiding the password form when signed-in with a linked account. !3381 + - Add links to CI setup documentation from project settings and builds pages. !3384 + - Fix an issue with width of project select dropdown. !3386 + - Remove redundant `require`s from Banzai files. !3391 + - Fix error 500 with cancel button on issuable edit form. !3392 + !3417 + - Fix background when editing a highlighted note. !3423 + - Remove tabstop from the WIP toggle links. !3426 + - Ensure private project snippets are not viewable by unauthorized people. + - Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402 + - Fixed issue with notification settings not saving. !3452 + +v 8.6.1 + - Add option to reload the schema before restoring a database backup. !2807 + - Display navigation controls on mobile. !3214 + - Fixed bug where participants would not work correctly on merge requests. !3329 + - Fix sorting issues by votes on the groups issues page results in SQL errors. !3333 + - Restrict notifications for confidential issues. !3334 + - Do not allow to move issue if it has not been persisted. !3340 + - Add a confirmation step before deleting an issuable. !3341 + - Fixes issue with signin button overflowing on mobile. !3342 + - Auto collapses the navigation sidebar when resizing. !3343 + - Fix build dependencies, when the dependency is a string. !3344 + - Shows error messages when trying to create label in dropdown menu. !3345 + - Fixes issue with assign milestone not loading milestone list. !3346 + - Fix an issue causing the Dashboard/Milestones page to be blank. !3348 v 8.6.0 - Add ability to move issue to another project @@ -33,7 +134,6 @@ v 8.6.0 - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - HTTP error pages work independently from location and config (Artem Sidorenko) - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - - Add option to reload the schema before restoring a database backup. !2807 - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Added omniauth-auth0 Gem (Daniel Carraro) @@ -65,6 +165,7 @@ v 8.6.0 - Add main language of a project in the list of projects (Tiago Botelho) - Add #upcoming filter to Milestone filter (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages + - Remove fork link closes all merge requests opened on source project (Florent Baldino) - Move group activity to separate page - Create external users which are excluded of internal and private projects unless access was explicitly granted - Continue parameters are checked to ensure redirection goes to the same instance @@ -72,7 +173,12 @@ v 8.6.0 - Canceled builds are now ignored in compound build status if marked as `allowed to fail` - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests - - Fix sorting issues by votes on the groups issues page results in SQL errors + +v 8.5.10 + - Fix a 2FA authentication spoofing vulnerability. + +v 8.5.9 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). v 8.5.8 - Bump Git version requirement to 2.7.4 @@ -215,6 +321,15 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.8 + - Fix a 2FA authentication spoofing vulnerability. + +v 8.4.7 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + +v 8.4.6 + - Bump Git version requirement to 2.7.4 + v 8.4.5 - No CE-specific changes @@ -328,6 +443,15 @@ v 8.4.0 - Add IP check against DNSBLs at account sign-up - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching +v 8.3.7 + - Fix a 2FA authentication spoofing vulnerability. + +v 8.3.6 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + +v 8.3.5 + - Bump Git version requirement to 2.7.4 + v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7540fa1afc..1f26a5d7ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,7 @@ - [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue weight](#issue-weight) - [Regression issues](#regression-issues) + - [Technical debt](#technical-debt) - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - [Merge request description format](#merge-request-description-format) @@ -242,6 +243,28 @@ addressed. [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue +### Technical debt + +In order to track things that can be improved in GitLab's codebase, we created +the ~"technical debt" label in [GitLab's issue tracker][ce-tracker]. + +This label should be added to issues that describe things that can be improved, +shortcuts that have been taken, code that needs refactoring, features that need +additional attention, and all other things that have been left behind due to +high velocity of development. + +Everyone can create an issue, though you may need to ask for adding a specific +label, if you do not have permissions to do it by yourself. Additional labels +can be combined with the `technical debt` label, to make it easier to schedule +the improvements for a release. + +Issues tagged with the `technical debt` label have the same priority like issues +that describe a new feature to be introduced in GitLab, and should be scheduled +for a release by the appropriate person. + +Make sure to mention the merge request that the `technical debt` issue is +associated with in the description of the issue. + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, @@ -425,7 +448,7 @@ merge request: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) -1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) +1. [Testing](doc/development/testing.md) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [SCSS styleguide][scss-styleguide] 1. [Shell commands](doc/development/shell_commands.md) created by GitLab diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index bc02b8685c..37c2961c24 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.11 +2.7.2 diff --git a/Gemfile b/Gemfile index cebd76e737..258b5612cd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.5.2' +gem 'rails', '4.2.6' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -8,7 +8,7 @@ gem 'responders', '~> 2.0' # Specify a sprockets version due to increased performance # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 -gem 'sprockets', '~> 3.3.5' +gem 'sprockets', '~> 3.6.0' # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -149,6 +149,10 @@ gem 'version_sorter', '~> 2.0.0' # Cache gem "redis-rails", '~> 4.0.0' +# Redis +gem 'redis', '~> 3.2' +gem 'connection_pool', '~> 2.0' + # Campfire integration gem 'tinder', '~> 1.10.0' @@ -214,7 +218,7 @@ gem 'jquery-rails', '~> 4.0.0' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store', '~> 1.2.0' +gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' @@ -229,14 +233,13 @@ 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.1.0', require: false + gem 'brakeman', '~> 3.2.0', require: false - gem "annotate", "~> 2.6.0" + gem "annotate", "~> 2.7.0" gem "letter_opener", '~> 1.1.2' gem 'quiet_assets', '~> 1.0.2' gem 'rerun', '~> 0.11.0' @@ -279,7 +282,7 @@ group :development, :test do gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' - gem 'teaspoon', '~> 1.0.0' + gem 'teaspoon', '~> 1.1.0' gem 'teaspoon-jasmine', '~> 2.2.0' gem 'spring', '~> 1.6.4' @@ -290,7 +293,7 @@ group :development, :test do gem 'rubocop', '~> 0.38.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'coveralls', '~> 0.8.2', require: false - gem 'simplecov', '~> 0.10.0', require: false + gem 'simplecov', '~> 0.11.0', require: false gem 'flog', require: false gem 'flay', require: false gem 'bundler-audit', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 16c09ab6e6..9da44a4658 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) + actionmailer (4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5.2) - actionview (= 4.2.5.2) - activesupport (= 4.2.5.2) + actionpack (4.2.6) + actionview (= 4.2.6) + activesupport (= 4.2.6) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5.2) - activesupport (= 4.2.5.2) + actionview (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5.2) - activesupport (= 4.2.5.2) + activejob (4.2.6) + activesupport (= 4.2.6) globalid (>= 0.3.0) - activemodel (4.2.5.2) - activesupport (= 4.2.5.2) + activemodel (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) - activerecord (4.2.5.2) - activemodel (= 4.2.5.2) - activesupport (= 4.2.5.2) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.5.2) + activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -51,8 +51,8 @@ GEM activerecord (>= 3.0) akismet (2.0.0) allocations (1.0.4) - annotate (2.6.10) - activerecord (>= 3.2, <= 4.3) + annotate (2.7.0) + activerecord (>= 3.2, < 6.0) rake (~> 10.4) arel (6.0.3) asana (0.4.0) @@ -84,24 +84,22 @@ GEM bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - brakeman (3.1.4) + brakeman (3.2.1) erubis (~> 2.6) - fastercsv (~> 1.5) haml (>= 3.0, < 5.0) highline (>= 1.6.20, < 2.0) - multi_json (~> 1.2) - ruby2ruby (>= 2.1.1, < 2.3.0) - ruby_parser (~> 3.7.0) + ruby2ruby (~> 2.3.0) + ruby_parser (~> 3.8.1) 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) - bullet (4.14.10) + bullet (5.0.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.9.0) - bundler-audit (0.4.0) + bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) @@ -128,9 +126,9 @@ GEM coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.1.0) + coffee-rails (4.1.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0, < 5.1.x) coffee-script (2.4.1) coffee-script-source execjs @@ -138,17 +136,16 @@ GEM colorize (0.7.7) concurrent-ruby (1.0.0) connection_pool (2.2.0) - coveralls (0.8.9) + coveralls (0.8.13) json (~> 1.8) - rest-client (>= 1.6.8, < 2) - simplecov (~> 0.10.0) + simplecov (~> 0.11.0) term-ansicolor (~> 1.3) thor (~> 0.19.1) tins (~> 1.6.0) crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) - css_parser (1.3.7) + css_parser (1.4.1) addressable d3_rails (3.5.11) railties (>= 3.1.0) @@ -178,8 +175,6 @@ GEM diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) - domain_name (0.5.25) - unf (>= 0.0.5, < 1.0.0) doorkeeper (2.2.2) railties (>= 3.2) dropzonejs-rails (0.7.2) @@ -208,7 +203,6 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json - fastercsv (1.5.5) ffaker (2.0.0) ffi (1.9.10) fission (0.5.0) @@ -328,8 +322,8 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.5.0.0) - railties (>= 3.2, < 5.0) + font-awesome-rails (4.5.0.1) + railties (>= 3.2, < 5.1) foreman (0.78.0) thor (~> 0.19.1) formatador (0.2.5) @@ -424,8 +418,6 @@ GEM nokogiri (~> 1.6.0) ruby_parser (~> 3.5) htmlentities (4.3.4) - http-cookie (1.0.2) - domain_name (~> 0.5) http_parser.rb (0.5.3) httparty (0.13.7) json (~> 1.8) @@ -467,8 +459,8 @@ GEM nokogiri (>= 1.5.9) macaddr (1.7.1) systemu (~> 2.6.2) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) mail_room (0.6.1) method_source (0.8.2) mime-types (1.25.1) @@ -483,7 +475,6 @@ GEM nested_form (0.3.2) net-ldap (0.12.1) net-ssh (3.0.1) - netrc (0.11.0) newrelic_rpm (3.14.1.311) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) @@ -568,8 +559,8 @@ GEM premailer (1.8.6) css_parser (>= 1.3.6) htmlentities (>= 4.0.0) - premailer-rails (1.9.0) - actionmailer (>= 3, < 5) + premailer-rails (1.9.2) + actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) pry (0.10.3) coderay (~> 1.1.0) @@ -598,16 +589,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.5.2) - actionmailer (= 4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) - activemodel (= 4.2.5.2) - activerecord (= 4.2.5.2) - activesupport (= 4.2.5.2) + rails (4.2.6) + actionmailer (= 4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + activemodel (= 4.2.6) + activerecord (= 4.2.6) + activesupport (= 4.2.6) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5.2) + railties (= 4.2.6) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -617,9 +608,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.5.2) - actionpack (= 4.2.5.2) - activesupport (= 4.2.5.2) + railties (4.2.6) + actionpack (= 4.2.6) + activesupport (= 4.2.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) @@ -655,15 +646,11 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.7) redis (>= 2.2) - request_store (1.2.1) + request_store (1.3.0) rerun (0.11.0) listen (~> 3.0) responders (2.1.1) railties (>= 4.2.0, < 5.1) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) rinku (1.7.3) rotp (2.1.1) rouge (1.10.1) @@ -706,10 +693,10 @@ GEM ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) - ruby2ruby (2.2.0) + ruby2ruby (2.3.0) ruby_parser (~> 3.1) sexp_processor (~> 4.0) - ruby_parser (3.7.2) + ruby_parser (3.8.1) sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) @@ -718,7 +705,7 @@ GEM safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) - sass (3.4.20) + sass (3.4.21) sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) sass (~> 3.1) @@ -742,7 +729,7 @@ GEM sentry-raven (0.15.6) faraday (>= 0.7.6) settingslogic (2.0.9) - sexp_processor (4.6.0) + sexp_processor (4.7.0) sham_rack (1.3.6) rack shoulda-matchers (2.8.0) @@ -757,7 +744,7 @@ GEM rufus-scheduler (>= 2.0.24) sidekiq (>= 4.0.0) simple_oauth (0.1.9) - simplecov (0.10.0) + simplecov (0.11.2) docile (~> 1.1.0) json (~> 1.8) simplecov-html (~> 0.10.0) @@ -789,12 +776,13 @@ GEM spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) - sprockets (3.3.5) + sprockets (3.6.0) + concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) state_machines (0.4.0) state_machines-activemodel (0.3.0) activemodel (~> 4.1) @@ -806,8 +794,8 @@ GEM systemu (2.6.5) task_list (1.0.2) html-pipeline - teaspoon (1.0.2) - railties (>= 3.2.5, < 5) + teaspoon (1.1.5) + railties (>= 3.2.5, < 6) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) temple (0.7.6) @@ -848,7 +836,7 @@ GEM underscore-rails (1.8.3) unf (0.1.4) unf_ext - unf_ext (0.0.7.1) + unf_ext (0.0.7.2) unicode-display_width (1.0.2) unicorn (4.9.0) kgio (~> 2.6) @@ -868,7 +856,7 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.4) rack (>= 1.0) - web-console (2.2.1) + web-console (2.3.0) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) railties (>= 4.0) @@ -900,7 +888,7 @@ DEPENDENCIES after_commit_queue akismet (~> 2.0) allocations (~> 1.0) - annotate (~> 2.6.0) + annotate (~> 2.7.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) @@ -910,7 +898,7 @@ DEPENDENCIES better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) - brakeman (~> 3.1.0) + brakeman (~> 3.2.0) browser (~> 1.0.0) bullet bundler-audit @@ -1005,16 +993,17 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.5.2) + rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) recaptcha redcarpet (~> 3.3.3) + redis (~> 3.2) redis-namespace redis-rails (~> 4.0.0) - request_store (~> 1.2.0) + request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) @@ -1035,7 +1024,7 @@ DEPENDENCIES shoulda-matchers (~> 2.8.0) sidekiq (~> 4.0) sidekiq-cron (~> 0.4.0) - simplecov (~> 0.10.0) + simplecov (~> 0.11.0) sinatra (~> 1.4.4) six (~> 0.2.0) slack-notifier (~> 1.2.0) @@ -1045,10 +1034,10 @@ DEPENDENCIES spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.0.0) spring-commands-teaspoon (~> 0.0.2) - sprockets (~> 3.3.5) + sprockets (~> 3.6.0) state_machines-activerecord (~> 0.3.0) task_list (~> 1.0.2) - teaspoon (~> 1.0.0) + teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) thin (~> 1.6.1) @@ -1064,3 +1053,6 @@ DEPENDENCIES web-console (~> 2.0) webmock (~> 1.21.0) wikicloth (= 0.8.1) + +BUNDLED WITH + 1.11.2 diff --git a/VERSION b/VERSION index cac7d91add..91ab1f99da 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.6.0-pre +8.7.0-pre diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 3c83450ce1..f01c67e947 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -43,6 +43,7 @@ #= require jquery.nicescroll #= require_tree . #= require fuzzaldrin-plus +#= require cropper window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 47b080406d..af4462ece3 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,5 +1,5 @@ class @AwardsHandler - constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @@ -22,8 +22,19 @@ class @AwardsHandler emoji = $(this) .find(".icon") .data "emoji" + + if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown" + awards_handler.addAward "thumbsdown" + + else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup" + awards_handler.addAward "thumbsup" + awards_handler.addAward emoji + didUserClickEmoji: (that, emoji) -> + if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") + $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 + showEmojiMenu: -> if $(".emoji-menu").length if $(".emoji-menu").is ".is-visible" @@ -34,7 +45,7 @@ class @AwardsHandler $("#emoji_search").focus() else $('.js-add-award').addClass "is-loading" - $.get "/emojis", (response) => + $.get @get_emojis_url, (response) => $('.js-add-award').removeClass "is-loading" $(".js-award-holder").append response setTimeout => @@ -105,7 +116,7 @@ class @AwardsHandler if origTitle authors = origTitle.split(', ') authors.push("me") - award_block.attr("title", authors.join(", ")) + award_block.attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> @@ -122,7 +133,7 @@ class @AwardsHandler nodes = [] nodes.push( - "" diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee index 79d750d184..0faa570ce1 100644 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -35,4 +35,18 @@ $.fn.requiresInput = -> $form.on 'change input', fieldSelector, requireInput $ -> - $('form.js-requires-input').requiresInput() + $form = $('form.js-requires-input') + $form.requiresInput() + + # Hide or Show the help block when creating a new project + # based on the option selected + hideOrShowHelpBlock = (form) -> + selected = $('.js-select-namespace option:selected') + if selected.length and selected.data('options-parent') is 'groups' + return form.find('.help-block').hide() + else if selected.length + form.find('.help-block').show() + + hideOrShowHelpBlock($form) + + $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form) diff --git a/app/assets/javascripts/compare.js.coffee b/app/assets/javascripts/compare.js.coffee new file mode 100644 index 0000000000..f20992ead3 --- /dev/null +++ b/app/assets/javascripts/compare.js.coffee @@ -0,0 +1,67 @@ +class @Compare + constructor: (@opts) -> + @source_loading = $ ".js-source-loading" + @target_loading = $ ".js-target-loading" + + $('.js-compare-dropdown').each (i, dropdown) => + $dropdown = $(dropdown) + + $dropdown.glDropdown( + selectable: true + fieldName: $dropdown.data 'field-name' + filterable: true + id: (obj, $el) -> + $el.data 'id' + toggleLabel: (obj, $el) -> + $el.text().trim() + clicked: (e, el) => + if $dropdown.is '.js-target-branch' + @getTargetHtml() + else if $dropdown.is '.js-source-branch' + @getSourceHtml() + else if $dropdown.is '.js-target-project' + @getTargetProject() + ) + + @initialState() + + initialState: -> + @getSourceHtml() + @getTargetHtml() + + getTargetProject: -> + $.ajax( + url: @opts.targetProjectUrl + data: + target_project_id: $("input[name='merge_request[target_project_id]']").val() + beforeSend: -> + $('.mr_target_commit').empty() + success: (html) -> + $('.js-target-branch-dropdown .dropdown-content').html html + ) + + getSourceHtml: -> + @sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit', + ref: $("input[name='merge_request[source_branch]']").val() + ) + + getTargetHtml: -> + @sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit', + target_project_id: $("input[name='merge_request[target_project_id]']").val() + ref: $("input[name='merge_request[target_branch]']").val() + ) + + sendAjax: (url, loading, target, data) -> + $target = $(target) + + $.ajax( + url: url + data: data + beforeSend: -> + loading.show() + $target.empty() + success: (html) -> + loading.hide() + $target.html html + $('.js-timeago', $target).timeago() + ) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f5e1ca9860..70fd6f50e9 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -146,15 +146,11 @@ class Dispatcher when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() - # If we haven't installed a custom shortcut handler, install the default one if not shortcut_handler new Shortcuts() initSearch: -> - opts = $('.search-autocomplete-opts') - path = opts.data('autocomplete-path') - project_id = opts.data('autocomplete-project-id') - project_ref = opts.data('autocomplete-project-ref') - new SearchAutocomplete(path, project_id, project_ref) + # Only when search form is present + new SearchAutocomplete() if $('.search').length diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee new file mode 100644 index 0000000000..df9bfdfa6c --- /dev/null +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -0,0 +1,152 @@ +class GitLabCrop + # Matches everything but the file name + FILENAMEREGEX = /^.*[\\\/]/ + + constructor: (input, opts = {}) -> + @fileInput = $(input) + + # We should rename to avoid spec to fail + # Form will submit the proper input filed with a file using FormData + @fileInput + .attr('name', "#{@fileInput.attr('name')}-trigger") + .attr('id', "#{@fileInput.attr('id')}-trigger") + + # Set defaults + { + @exportWidth = 200 + @exportHeight = 200 + @cropBoxWidth = 200 + @cropBoxHeight = 200 + @form = @fileInput.parents('form') + + # Required params + @filename + @previewImage + @modalCrop + @pickImageEl + @uploadImageBtn + @modalCropImg + } = opts + + # Ensure needed elements are jquery objects + # If selector is provided we will convert them to a jQuery Object + @filename = @getElement(@filename) + @previewImage = @getElement(@previewImage) + @pickImageEl = @getElement(@pickImageEl) + + # Modal elements usually are outside the @form element + @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop + @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn + @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg + + @cropActionsBtn = @modalCrop.find('[data-method]') + + @bindEvents() + + getElement: (selector) -> + $(selector, @form) + + bindEvents: -> + _this = @ + @fileInput.on 'change', (e) -> + _this.onFileInputChange(e, @) + + @pickImageEl.on 'click', @onPickImageClick + @modalCrop.on 'shown.bs.modal', @onModalShow + @modalCrop.on 'hidden.bs.modal', @onModalHide + @uploadImageBtn.on 'click', @onUploadImageBtnClick + @cropActionsBtn.on 'click', (e) -> + btn = @ + _this.onActionBtnClick(btn) + @croppedImageBlob = null + + onPickImageClick: => + @fileInput.trigger('click') + + onModalShow: => + _this = @ + @modalCropImg.cropper( + viewMode: 1 + center: false + aspectRatio: 1 + modal: true + scalable: false + rotatable: false + zoomable: true + dragMode: 'move' + guides: false + zoomOnTouch: false + zoomOnWheel: false + cropBoxMovable: false + cropBoxResizable: false + toggleDragModeOnDblclick: false + built: -> + $image = $(@) + container = $image.cropper 'getContainerData' + cropBoxWidth = _this.cropBoxWidth; + cropBoxHeight = _this.cropBoxHeight; + + $image.cropper('setCropBoxData', + width: cropBoxWidth, + height: cropBoxHeight, + left: (container.width - cropBoxWidth) / 2, + top: (container.height - cropBoxHeight) / 2 + ) + ) + + + onModalHide: => + @modalCropImg + .attr('src', '') # Remove attached image + .cropper('destroy') # Destroy cropper instance + + onUploadImageBtnClick: (e) => + e.preventDefault() + @setBlob() + @setPreview() + @modalCrop.modal('hide') + @fileInput.val('') + + onActionBtnClick: (btn) -> + data = $(btn).data() + + if @modalCropImg.data('cropper') && data.method + result = @modalCropImg.cropper data.method, data.option + + onFileInputChange: (e, input) -> + @readFile(input) + + readFile: (input) -> + _this = @ + reader = new FileReader + reader.onload = -> + _this.modalCropImg.attr('src', reader.result) + _this.modalCrop.modal('show') + + reader.readAsDataURL(input.files[0]) + + dataURLtoBlob: (dataURL) -> + binary = atob(dataURL.split(',')[1]) + array = [] + for v, k in binary + array.push(binary.charCodeAt(k)) + new Blob([new Uint8Array(array)], type: 'image/png') + + setPreview: -> + @previewImage.attr('src', @dataURL) + filename = @fileInput.val().replace(FILENAMEREGEX, '') + @filename.text(filename) + + setBlob: -> + @dataURL = @modalCropImg.cropper('getCroppedCanvas', + width: 200 + height: 200 + ).toDataURL('image/png') + @croppedImageBlob = @dataURLtoBlob(@dataURL) + + getBlob: -> + @croppedImageBlob + +$.fn.glCrop = (opts) -> + return @.each -> + $(@).data('glcrop', new GitLabCrop(@, opts)) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 960585245d..ee1d0fad28 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,23 +1,48 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] + ARROW_KEY_CODES = [38, 40] + HAS_VALUE_CLASS = "has-value" - constructor: (@dropdown, @options) -> - @input = @dropdown.find(".dropdown-input .dropdown-input-field") + constructor: (@input, @options) -> + { + @filterInputBlur = true + } = @options + + $inputContainer = @input.parent() + $clearButton = $inputContainer.find('.js-dropdown-input-clear') + + # Clear click + $clearButton.on 'click', (e) => + e.preventDefault() + e.stopPropagation() + @input + .val('') + .trigger('keyup') + .focus() # Key events timeout = "" @input.on "keyup", (e) => - if e.keyCode is 13 && @input.val() isnt "" + keyCode = e.which + + return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 + + if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS + $inputContainer.addClass HAS_VALUE_CLASS + else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS + $inputContainer.removeClass HAS_VALUE_CLASS + + if keyCode is 13 and @input.val() isnt "" if @options.enterCallback @options.enterCallback() return clearTimeout timeout timeout = setTimeout => - blur_field = @shouldBlur e.keyCode + blur_field = @shouldBlur keyCode search_text = @input.val() - if blur_field + if blur_field and @filterInputBlur @input.blur() if @options.remote @@ -32,14 +57,30 @@ class GitLabDropdownFilter filter: (search_text) -> data = @options.data() - results = data - if search_text isnt "" - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) + if data? + results = data - @options.callback results + if search_text isnt '' + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + + @options.callback results + else + elements = @options.elements() + + if search_text + elements.each -> + $el = $(@) + matches = fuzzaldrinPlus.match($el.text().trim(), search_text) + + if matches.length + $el.show() + else + $el.hide() + else + elements.show() class GitLabDropdownRemote constructor: (@dataEndpoint, @options) -> @@ -76,39 +117,74 @@ class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + currentIndex = -1 + + FILTER_INPUT = '.dropdown-input .dropdown-input-field' constructor: (@el, @options) -> - self = @ @dropdown = $(@el).parent() - search_fields = if @options.search then @options.search.fields else []; + + # Set Defaults + { + # If no input is passed create a default one + @filterInput = @getElement(FILTER_INPUT) + @highlight = false + @filterInputBlur = true + @enterCallback = true + } = @options + + self = @ + + # If selector was passed + if _.isString(@filterInput) + @filterInput = @getElement(@filterInput) + + searchFields = if @options.search then @options.search.fields else []; if @options.data - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data + # If data is an array + if _.isArray @options.data + @fullData = @options.data + @parseData @options.data + else + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data - @parseData @fullData - } + @parseData @fullData + } - # Init filiterable + # Init filterable if @options.filterable - @filter = new GitLabDropdownFilter @dropdown, + @filter = new GitLabDropdownFilter @filterInput, + filterInputBlur: @filterInputBlur remote: @options.filterRemote query: @options.data - keys: @options.search.fields + keys: searchFields + elements: => + selector = '.dropdown-content li:not(.divider)' + + if @dropdown.find('.dropdown-toggle-page').length + selector = ".dropdown-page-one #{selector}" + + return $(selector) data: => return @fullData callback: (data) => + currentIndex = -1 @parseData data enterCallback: => - @selectFirstRow() + if @enterCallback + @selectRowAtIndex 0 # Event listeners + @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden + @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate if @dropdown.find(".dropdown-toggle-page").length @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @@ -124,10 +200,15 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> - self.rowClicked $(@) + $el = $(@) + selected = self.rowClicked $el if self.options.clicked - self.options.clicked() + self.options.clicked(selected, $el, e) + + # Finds an element inside wrapper element + getElement: (selector) -> + @dropdown.find selector toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -157,15 +238,29 @@ class GitLabDropdown @appendMenu(full_html) + shouldPropagate: (e) => + if @options.multiSelect + $target = $(e.target) + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + e.stopPropagation() + return false + else + return true + opened: => + @addArrowKeyEvent() + contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() if @options.filterable - @dropdown.find(".dropdown-input-field").focus() + @filterInput.focus() - hidden: => + @dropdown.trigger('shown.gl.dropdown') + + hidden: (e) => + @removeArrayKeyEvent() if @options.filterable @dropdown .find(".dropdown-input-field") @@ -176,6 +271,11 @@ class GitLabDropdown if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS + if @options.hidden + @options.hidden.call(@,e) + + @dropdown.trigger('hidden.gl.dropdown') + # Render the full menu renderMenu: (html) -> @@ -200,81 +300,194 @@ class GitLabDropdown renderItem: (data) -> html = "" + # Divider return "
  • " if data is "divider" + # Separator is a full-width divider + return "
  • " if data is "separator" + + # Header + return "" if data.header? + if @options.renderRow # Call the render function html = @options.renderRow(data) else - selected = if @options.isSelected then @options.isSelected(data) else false - url = if @options.url then @options.url(data) else "#" - text = if @options.text then @options.text(data) else "" + if not selected + value = if @options.id then @options.id(data) else data.id + fieldName = @options.fieldName + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + if field.length + selected = true + + # Set URL + if @options.url? + url = @options.url(data) + else + url = if data.url? then data.url else '#' + + # Set Text + if @options.text? + text = @options.text(data) + else + text = if data.text? then data.text else '' + cssClass = ""; if selected cssClass = "is-active" - html = "
  • " - html += "" - html += text - html += "" - html += "
  • " + if @highlight + text = @highlightTextMatches(text, @filterInput.val()) + + html = "
  • + + #{text} + +
  • " return html + highlightTextMatches: (text, term) -> + occurrences = fuzzaldrinPlus.match(text, term) + text.split('').map((character, i) -> + if i in occurrences then "#{character}" else character + ).join('') + noResults: -> - html = "
  • " - html += "" - html += "No matching results." - html += "" - html += "
  • " + html = "" + + highlightRow: (index) -> + if @filterInput.val() isnt "" + selector = '.dropdown-content li:first-child a' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one .dropdown-content li:first-child a" + + @getElement(selector).addClass 'is-focused' rowClicked: (el) -> fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}']") + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) + el.removeClass(ACTIVE_CLASS) field.remove() - else - fieldName = @options.fieldName - selectedIndex = el.parent().index() - if @renderedData - selectedObject = @renderedData[selectedIndex] - value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - - if !value? - field.remove() - - if @options.multiSelect - oldValue = field.val() - if oldValue - value = "#{oldValue},#{value}" - else - @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS - - # Toggle active class for the tick mark - el.toggleClass "is-active" # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + else + selectedObject + else + if !value? + field.remove() + if not @options.multiSelect + @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS + @dropdown.parent().find("input[name='#{fieldName}']").remove() + + # Toggle active class for the tick mark + el.addClass ACTIVE_CLASS + + # Toggle the dropdown label + if @options.toggleLabel + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) if value? - if !field.length + if !field.length and fieldName # Create hidden input for form - input = "" + input = "" + if @options.inputId? + input = $(input) + .attr('id', @options.inputId) @dropdown.before input + else + field.val value - @dropdown.parent().find("input[name='#{fieldName}']").val value + return selectedObject + + selectRowAtIndex: (index) -> + selector = ".dropdown-content li:not(.divider):eq(#{index}) a" - selectFirstRow: -> - selector = '.dropdown-content li:first-child a' if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one .dropdown-content li:first-child a" + selector = ".dropdown-page-one #{selector}" - # similute a click on the first link - $(selector).trigger "click" + # simulate a click on the first link + $(selector, @dropdown).trigger "click" + + addArrowKeyEvent: -> + ARROW_KEY_CODES = [38, 40] + $input = @dropdown.find(".dropdown-input-field") + + selector = '.dropdown-content li:not(.divider)' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one #{selector}" + + $('body').on 'keydown', (e) => + currentKeyCode = e.which + + if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0 + e.preventDefault() + e.stopImmediatePropagation() + + PREV_INDEX = currentIndex + $listItems = $(selector, @dropdown) + + # if @options.filterable + # $input.blur() + + if currentKeyCode is 40 + # Move down + currentIndex += 1 if currentIndex < ($listItems.length - 1) + else if currentKeyCode is 38 + # Move up + currentIndex -= 1 if currentIndex > 0 + + @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX + + return false + + if currentKeyCode is 13 + @selectRowAtIndex currentIndex + + removeArrayKeyEvent: -> + $('body').off 'keydown' + + highlightRowAtIndex: ($listItems, index) -> + # Remove the class for the previously focused row + $('.is-focused', @dropdown).removeClass 'is-focused' + + # Update the class for the row at the specific index + $listItem = $listItems.eq(index) + $listItem.find('a:first-child').addClass "is-focused" + + # Dropdown content scroll area + $dropdownContent = $listItem.closest('.dropdown-content') + dropdownScrollTop = $dropdownContent.scrollTop() + dropdownContentHeight = $dropdownContent.outerHeight() + dropdownContentTop = $dropdownContent.prop('offsetTop') + dropdownContentBottom = dropdownContentTop + dropdownContentHeight + + # Get the offset bottom of the list item + listItemHeight = $listItem.outerHeight() + listItemTop = $listItem.prop('offsetTop') + listItemBottom = listItemTop + listItemHeight + + if listItemBottom > dropdownContentBottom + dropdownScrollTop + # Scroll the dropdown content down + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom) + else if listItemTop < dropdownContentTop + dropdownScrollTop + # Scroll the dropdown content up + $dropdownContent.scrollTop(listItemTop - dropdownContentTop) $.fn.glDropdown = (opts) -> return @.each -> - new GitLabDropdown @, opts + if (!$.data @, 'glDropdown') + $.data(@, 'glDropdown', new GitLabDropdown @, opts) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index d6d09b36d8..2f19513a83 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -1,8 +1,7 @@ class @IssuableContext - constructor: -> + constructor: (currentUser) -> @initParticipants() - - new UsersSelect() + new UsersSelect(currentUser) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $(".issuable-sidebar .inline-update").on "change", "select", -> @@ -10,11 +9,21 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $(document).on "click",".edit-link", (e) -> - block = $(@).parents('.block') - block.find('.selectbox').show() - block.find('.value').hide() - block.find('.js-select2').select2("open") + $(document).off("click", ".edit-link").on "click",".edit-link", (e) -> + $block = $(@).parents('.block') + $selectbox = $block.find('.selectbox') + if $selectbox.is(':visible') + $selectbox.hide() + $block.find('.value').show() + else + $selectbox.show() + $block.find('.value').hide() + + if $selectbox.is(':visible') + setTimeout (-> + $block.find('.dropdown-menu-toggle').trigger 'click' + ), 0 + $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index d663e34871..946d83b7bd 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -6,25 +6,10 @@ class @Issue constructor: -> # Prevent duplicate event bindings @disableTaskList() - @fixAffixScroll() if $('a.btn-close').length @initTaskList() @initIssueBtnEventListeners() - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -49,7 +34,7 @@ class @Issue issueStatus = if isClose then 'close' else 'open' new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> - if data.saved + if 'id' of data $(document).trigger('issuable:change'); if isClose $('a.btn-close').addClass('hidden') diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 1127b28926..0d9f2094c2 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,7 +1,6 @@ @Issues = init: -> Issues.initSearch() - Issues.initSelects() Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -17,18 +16,9 @@ $(this).html totalIssues - 1 reload: -> - Issues.initSelects() Issues.initChecks() $('#filter_issue_search').val($('#issue_search').val()) - initSelects: -> - $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true) - $("#milestone_id, #assignee_id, #label_name").on "change", -> - $(this).closest("form").submit() - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) @@ -36,6 +26,20 @@ $(".selected_issue").bind "change", Issues.checkChanged + # Update state filters if present in page + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = $(this).attr 'href' + $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) + # Make sure we trigger ajax request only after user stop typing initSearch: -> @timer = null @@ -64,6 +68,7 @@ # Change url so if user reload a page - search results are saved history.replaceState {page: issuesUrl}, document.title, issuesUrl Issues.reload() + Issues.updateStateFilters() dataType: "json" checkChanged: -> diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index f3cb1e3bc0..d1fe116397 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -4,31 +4,81 @@ class @LabelsSelect $dropdown = $(dropdown) projectId = $dropdown.data('project-id') labelUrl = $dropdown.data('labels') + issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel + if selectedLabel? selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') defaultLabel = $dropdown.data('default-label') + abilityName = $dropdown.data('ability-name') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() if newLabelField.length + $newLabelCreateButton = $('.js-new-label-btn') + $colorPreview = $('.js-dropdown-label-color-preview') $newLabelError = $dropdown.parent().find('.js-label-error') $newLabelError.hide() + # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on 'click', (e) -> + + issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? + if issueUpdateURL + labelHTMLTemplate = _.template( + '<% _.each(labels, function(label){ %> + issues?label_name=<%= label.title %>"> + + <%= label.title %> + + + <% }); %>' + ); + labelNoneHTMLTemplate = _.template('
    None
    ') + + if newLabelField.length and $dropdown.hasClass 'js-extra-options' + $('.suggest-colors-dropdown a').on "click", (e) -> e.preventDefault() e.stopPropagation() - newColorField.val $(this).data('color') - $('.js-dropdown-label-color-preview') + newColorField + .val($(this).data('color')) + .trigger('change') + $colorPreview .css 'background-color', $(this).data('color') + .parent() .addClass 'is-active' - $('.js-new-label-btn').on 'click', (e) -> + # Cancel button takes back to first page + resetForm = -> + newLabelField + .val '' + .trigger 'change' + newColorField + .val '' + .trigger 'change' + $colorPreview + .css 'background-color', '' + .parent() + .removeClass 'is-active' + + $('.dropdown-menu-back').on 'click', -> + resetForm() + + $('.js-cancel-label-btn').on 'click', (e) -> e.preventDefault() e.stopPropagation() + resetForm() + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + # Listen for change and keyup events on label and color field + # This allows us to enable the button when ready + enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' $newLabelError.hide() $('.js-new-label-btn').disable() @@ -47,39 +97,118 @@ class @LabelsSelect else $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + $newLabelCreateButton.enable() + else + $newLabelCreateButton.disable() + + newLabelField.on 'keyup change', enableLabelCreateButton + + newColorField.on 'keyup change', enableLabelCreateButton + + # Send the API call to create the label + $newLabelCreateButton + .disable() + .on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + + if newLabelField.val() isnt '' and newColorField.val() isnt '' + $newLabelError.hide() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + + saveLabelData = -> + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']") + .map(-> + @value + ).get() + data = {} + data[abilityName] = {} + data[abilityName].label_ids = selected + if not selected.length + data[abilityName].label_ids = [''] + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + url: issueUpdateURL + dataType: 'JSON' + data: data + ).done (data) -> + $loading.fadeOut() + $dropdown.trigger('loaded.gl.dropdown') + $selectbox.hide() + data.issueURLSplit = issueURLSplit + labelCount = 0 + if data.labels.length + template = labelHTMLTemplate(data) + labelCount = data.labels.length + else + template = labelNoneHTMLTemplate() + $value + .removeAttr('style') + .html(template) + $sidebarCollapsedValue.text(labelCount) + + $value + .find('a') + .each((i) -> + setTimeout(=> + glAnimate($(@), 'pulse') + ,200 * i + ) + ) + + $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: labelUrl ).done (data) -> - if showNo - data.unshift( - id: 0 - title: 'No Label' - ) + if $dropdown.hasClass 'js-extra-options' + if showNo + data.unshift( + id: 0 + title: 'No Label' + ) - if showAny - data.unshift( - isAny: true - title: 'Any Label' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' + if showAny + data.unshift( + isAny: true + title: 'Any Label' + ) + if data.length > 2 + data.splice 2, 0, 'divider' callback data + renderRow: (label) -> - if $.isArray(selectedLabel) - selected = '' - $.each selectedLabel, (i, selectedLbl) -> - selectedLbl = selectedLbl.trim() - if selected is '' and label.title is selectedLbl - selected = 'is-active' - else - selected = if label.title is selectedLabel then 'is-active' else '' + selectedClass = '' + if $selectbox.find("input[type='hidden']\ + [name='#{$dropdown.data('field-name')}']\ + [value='#{label.id}']").length + selectedClass = 'is-active' + + color = if label.color? then "" else "" "
  • - + + #{color} #{label.title}
  • " @@ -87,6 +216,7 @@ class @LabelsSelect search: fields: ['title'] selectable: true + toggleLabel: (selected) -> if selected and selected.title isnt 'Any Label' selected.title @@ -96,15 +226,33 @@ class @LabelsSelect id: (label) -> if label.isAny? '' - else + else if $dropdown.hasClass "js-filter-submit" label.title - clicked: -> + else + label.id + + hidden: -> + $selectbox.hide() + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + if $dropdown.hasClass 'js-multiselect' + saveLabelData() + + multiSelect: $dropdown.hasClass 'js-multiselect' + clicked: (label) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is page is 'projects:merge_requests:index' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabel = label.title + Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + if $dropdown.hasClass 'js-multiselect' + return + else + saveLabelData() ) diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee new file mode 100644 index 0000000000..8f892b5a2b --- /dev/null +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -0,0 +1,13 @@ +((w) -> + + w.glAnimate = ($el, animation, done) -> + $el + .removeClass() + .addClass(animation + ' animated') + .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> + $(this).removeClass() + return + return + return + +) window \ No newline at end of file diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee new file mode 100644 index 0000000000..9e28353ac3 --- /dev/null +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -0,0 +1,35 @@ +((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + # Hide the notification after X amount of seconds + setTimeout -> + notification.close() + , 8000 + + if onclick + notification.onclick = onclick + + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> + opts = + body: body + icon: icon + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notificationGranted message, opts, onclick + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notificationGranted message, opts, onclick + + w.notify = notifyMe + w.notifyPermissions = notifyPermissions +) window diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee new file mode 100644 index 0000000000..abd556e0b4 --- /dev/null +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -0,0 +1,31 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.getUrlParameter = (sParam) -> + sPageURL = decodeURIComponent(window.location.search.substring(1)) + sURLVariables = sPageURL.split('&') + sParameterName = undefined + i = 0 + while i < sURLVariables.length + sParameterName = sURLVariables[i].split('=') + if sParameterName[0] is sParam + return if sParameterName[1] is undefined then true else sParameterName[1] + i++ + + # # + # @param {Object} params - url keys and value to merge + # @param {String} url + # # + w.gl.utils.mergeUrlParams = (params, url) -> + newUrl = decodeURIComponent(url) + for paramName, paramValue of params + pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" + if url.search(pattern) >= 0 + newUrl = newUrl.replace pattern, "$1#{paramValue}$2" + else + newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + newUrl + +) window diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 6af5a48a0b..1f46e33142 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -15,8 +15,6 @@ class @MergeRequest this.$('.show-all-commits').on 'click', => this.showAllCommits() - @fixAffixScroll(); - @initTabs() # Prevent duplicate event bindings @@ -30,20 +28,6 @@ class @MergeRequest $: (selector) -> this.$el.find(selector) - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTabs: -> if @opts.action != 'new' # `MergeRequests#new` has no tab-persisting or lazy-loading behavior diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 839e6ec2c0..9946249adb 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -73,7 +73,8 @@ class @MergeRequestTabs @expandView() else if action == 'diffs' @loadDiff($target.attr('href')) - @shrinkView() + if bp? and bp.getBreakpointSize() isnt 'lg' + @shrinkView() else if action == 'builds' @loadBuilds($target.attr('href')) @expandView() diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343..065626beeb 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -2,13 +2,29 @@ class @MergeRequestWidget # Initialize MergeRequestWidget behavior # # check_enable - Boolean, whether to check automerge status - # url_to_automerge_check - String, URL to use to check automerge status - # current_status - String, current automerge status - # ci_enable - Boolean, whether a CI service is enabled - # url_to_ci_check - String, URL to use to check CI status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status # + constructor: (@opts) -> - modal = $('#modal_merge_info').modal(show: false) + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = true + clearInterval @fetchBuildStatusInterval + + @clearEventListeners() + @addEventListeners() + @pollCIStatus() + notifyPermissions() + + clearEventListeners: -> + $(document).off 'page:change.merge_request' + + addEventListeners: -> + $(document).on 'page:change.merge_request', => + if $('body').data('page') isnt 'projects:merge_requests:show' + clearInterval @fetchBuildStatusInterval + @clearEventListeners() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -27,18 +43,71 @@ class @MergeRequestWidget dataType: 'json' getMergeStatus: -> - $.get @opts.url_to_automerge_check, (data) -> + $.get @opts.merge_check_url, (data) -> $('.mr-state-widget').replaceWith(data) - getCiStatus: -> - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status - if data.coverage - this.showCiCoverage data.coverage - , 'json' + ciLabelForStatus: (status) -> + if status is 'success' + 'passed' + else + status - showCiState: (state) -> + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 10000 + + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + + $.getJSON @opts.ci_status_url, (data) => + @readyForCICheck = true + + if @firstCICheck + @firstCICheck = false + @opts.ci_status = data.status + + if @opts.ci_status is '' + @opts.ci_status = data.status + return + + if data.status isnt @opts.ci_status and data.status? + @showCIStatus data.status + if data.coverage + @showCICoverage data.coverage + + if showNotification + status = @ciLabelForStatus(data.status) + + if status is "preparing" + title = @opts.ci_title.preparing + status = status.charAt(0).toUpperCase() + status.slice(1); + message = @opts.ci_message.preparing.replace('{{status}}', status) + else + title = @opts.ci_title.normal + message = @opts.ci_message.normal.replace('{{status}}', status) + + title = title.replace('{{status}}', status) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + title, + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + + @opts.ci_status = data.status + + showCIStatus: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -48,13 +117,17 @@ class @MergeRequestWidget @setMergeButtonClass('btn-danger') when "running", "pending" @setMergeButtonClass('btn-warning') + when "success" + @setMergeButtonClass('btn-create') else $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') - showCiCoverage: (coverage) -> + showCICoverage: (coverage) -> text = 'Coverage ' + coverage + '%' $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request').removeClass("btn-create").addClass(css_class) + $('.accept_merge_request') + .removeClass('btn-danger btn-warning btn-create') + .addClass(css_class) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index e17a1adb64..6bd4e885a0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,36 +1,65 @@ class @MilestoneSelect - constructor: -> + constructor: (currentProject) -> + if currentProject? + _this = @ + @currentProject = JSON.parse(currentProject) $('.js-milestone-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') milestonesUrl = $dropdown.data('milestones') + issueUpdateURL = $dropdown.data('issueUpdate') selectedMilestone = $dropdown.data('selected') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') + showUpcoming = $dropdown.data('show-upcoming') useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') + issuableId = $dropdown.data('issuable-id') + abilityName = $dropdown.data('ability-name') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() + + if issueUpdateURL + milestoneLinkTemplate = _.template( + '<%= title %>' + ) + + milestoneLinkNoneTemplate = '
    None
    ' $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: milestonesUrl ).done (data) -> - if showNo - data.unshift( - id: '0' - title: 'No Milestone' - ) - + extraOptions = [] if showAny - data.unshift( - isAny: true + extraOptions.push( + id: 0 + name: '' title: 'Any Milestone' ) - if data.length > 2 - data.splice 2, 0, 'divider' + if showNo + extraOptions.push( + id: -1 + name: 'No Milestone' + title: 'No Milestone' + ) - callback(data) + if showUpcoming + extraOptions.push( + id: -2 + name: '#upcoming' + title: 'Upcoming' + ) + + if extraOptions.length > 2 + extraOptions.push 'divider' + + callback(extraOptions.concat(data)) filterable: true search: fields: ['title'] @@ -45,21 +74,57 @@ class @MilestoneSelect milestone.title id: (milestone) -> if !useId - if !milestone.isAny? - milestone.title - else - '' + milestone.name else milestone.id isSelected: (milestone) -> - milestone.title is selectedMilestone - clicked: -> + milestone.name is selectedMilestone + hidden: -> + $selectbox.hide() + + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + clicked: (selected) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is page is 'projects:merge_requests:index' + if $dropdown.hasClass 'js-filter-bulk-update' + return + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + if selected.name? + selectedMilestone = selected.name + else + selectedMilestone = '' Issues.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' + else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() + else + selected = $selectbox + .find('input[type="hidden"]') + .val() + data = {} + data[abilityName] = {} + data[abilityName].milestone_id = selected + $loading + .fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $loading.fadeOut() + $selectbox.hide() + $value.removeAttr('style') + if data.milestone? + data.milestone.namespace = _this.currentProject.namespace + data.milestone.path = _this.currentProject.path + $value.html(milestoneLinkTemplate(data.milestone)) + $sidebarCollapsedValue.find('span').text(data.milestone.title) + else + $value.html(milestoneLinkNoneTemplate) + $sidebarCollapsedValue.find('span').text('No') ) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ff06c57f2b..86e3b860fc 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -251,13 +251,11 @@ class @Notes Sets some hidden fields in the form. ### setupMainTargetNoteForm: -> - # find the form form = $(".js-new-note-form") - # insert the form after the button - form.clone().replaceAll $(".js-main-target-form") - form = form.prev("form") + # Set a global clone of the form for later cloning + @formClone = form.clone() # show the form @setupNoteForm(form) @@ -266,9 +264,7 @@ class @Notes form.removeClass "js-new-note-form" form.addClass "js-main-target-form" - # remove unnecessary fields and buttons form.find("#note_line_code").remove() - form.find(".js-close-discussion-note-form").remove() ### General note form setup. @@ -297,7 +293,14 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" + textarea.on 'focus', -> + $(this).closest('.md-area').addClass 'is-focused' + + textarea.on 'blur', -> + $(this).closest('.md-area').removeClass 'is-focused' + autosize(textarea) + new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -307,7 +310,6 @@ class @Notes ] # remove notify commit author checkbox for non-commit notes - form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit" GitLab.GfmAutoComplete.setup() new DropzoneInput(form) form.show() @@ -455,15 +457,15 @@ class @Notes Shows the note form below the notes. ### replyToDiscussionNote: (e) => - form = $(".js-new-note-form") + form = @formClone.clone() replyLink = $(e.target).closest(".js-discussion-reply-button") replyLink.hide() # insert the form after the button - form.clone().insertAfter replyLink + replyLink.after form # show the form - @setupDiscussionNoteForm(replyLink, replyLink.next("form")) + @setupDiscussionNoteForm(replyLink, form) ### Shows the diff or discussion form and does some setup on it. @@ -488,7 +490,9 @@ class @Notes .text(form.find('.js-close-discussion-note-form').data('cancel-text')) @setupNoteForm form form.find(".js-note-text").focus() - form.addClass "js-discussion-note-form" + form + .removeClass('js-main-target-form') + .addClass("discussion-form js-discussion-note-form") ### Called when clicking on the "add a comment" button on the side of a diff line. @@ -498,9 +502,8 @@ class @Notes ### addDiffNote: (e) => e.preventDefault() - link = e.currentTarget - form = $(".js-new-note-form") - row = $(link).closest("tr") + $link = $(e.currentTarget) + row = $link.closest("tr") nextRow = row.next() hasNotes = nextRow.is(".notes_holder") addForm = false @@ -509,7 +512,7 @@ class @Notes # In parallel view, look inside the correct left/right pane if @isParallelView() - lineType = $(link).data("lineType") + lineType = $link.data("lineType") targetContent += "." + lineType rowCssToAdd = "" @@ -531,11 +534,11 @@ class @Notes addForm = true if addForm - newForm = form.clone() + newForm = @formClone.clone() newForm.appendTo row.next().find(targetContent) # show the form - @setupDiscussionNoteForm $(link), newForm + @setupDiscussionNoteForm $link, newForm ### Called in response to "cancel" on a diff note form. @@ -560,7 +563,6 @@ class @Notes cancelDiscussionForm: (e) => e.preventDefault() - form = $(".js-new-note-form") form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 20f8744055..ae87c6c4e4 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,5 +1,9 @@ class @Profile - constructor: -> + constructor: (opts = {}) -> + { + @form = $('.edit-user') + } = opts + # Automatically submit the Preferences form when any of its radio buttons change $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $(this).parents('form').submit() @@ -17,14 +21,46 @@ class @Profile $('.update-notifications').on 'ajax:complete', -> $(this).find('.btn-save').enable() - $('.js-choose-user-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-user-avatar-input").click() + @bindEvents() - $('.js-user-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) + cropOpts = + filename: '.js-avatar-filename' + previewImage: '.avatar-image .avatar' + modalCrop: '.modal-profile-crop' + pickImageEl: '.js-choose-user-avatar-button' + uploadImageBtn: '.js-upload-user-avatar' + modalCropImg: '.modal-profile-crop-image' + + @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop' + + bindEvents: -> + @form.on 'submit', @onSubmitForm + + onSubmitForm: (e) => + e.preventDefault() + @saveForm() + + saveForm: -> + self = @ + + formData = new FormData(@form[0]) + formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png') + + $.ajax + url: @form.attr('action') + type: @form.attr('method') + data: formData + dataType: "json" + processData: false + contentType: false + success: (response) -> + new Flash(response.message, 'notice') + error: (jqXHR) -> + new Flash(jqXHR.responseJSON.message, 'alert') + complete: -> + window.scrollTo 0, 0 + # Enable submit button after requests ends + self.form.find(':input[disabled]').enable() $ -> # Extract the SSH Key title from its comment diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee new file mode 100644 index 0000000000..6740355434 --- /dev/null +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -0,0 +1,55 @@ +class @Sidebar + constructor: (currentUser) -> + @addEventListeners() + + addEventListeners: -> + $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked) + $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden) + $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) + $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) + + sidebarDropdownLoading: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + i = $sidebarCollapsedIcon.find('i') + $loading = $('') + if img.length + img.before($loading) + img.hide() + else if i.length + i.before($loading) + i.hide() + + sidebarDropdownLoaded: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + $sidebarCollapsedIcon.find('i.fa-spin').remove() + i = $sidebarCollapsedIcon.find('i') + if img.length + img.show() + else + i.show() + + + sidebarCollapseClicked: (e) -> + e.preventDefault() + $block = $(@).closest('.block') + + $('aside') + .find('.gutter-toggle') + .trigger('click') + $editLink = $block.find('.edit-link') + + if $editLink.length + $editLink.trigger('click') + $block.addClass('collapse-after-update') + $('.page-with-sidebar').addClass('with-overlay') + + sidebarDropdownHidden: (e) -> + $block = $(@).closest('.block') + if $block.hasClass('collapse-after-update') + $block.removeClass('collapse-after-update') + $('.page-with-sidebar').removeClass('with-overlay') + $('aside') + .find('.gutter-toggle') + .trigger('click') \ No newline at end of file diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index c180136526..6a7b4ad1db 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,11 +1,296 @@ class @SearchAutocomplete - constructor: (search_autocomplete_path, project_id, project_ref) -> - project_id = '' unless project_id - project_ref = '' unless project_ref - query = "?project_id=" + project_id + "&project_ref=" + project_ref - $("#search").autocomplete - source: search_autocomplete_path + query - minLength: 1 - select: (event, ui) -> - location.href = ui.item.url + KEYCODE = + ESCAPE: 27 + BACKSPACE: 8 + ENTER: 13 + + constructor: (opts = {}) -> + { + @wrap = $('.search') + + @optsEl = @wrap.find('.search-autocomplete-opts') + @autocompletePath = @optsEl.data('autocomplete-path') + @projectId = @optsEl.data('autocomplete-project-id') || '' + @projectRef = @optsEl.data('autocomplete-project-ref') || '' + + } = opts + + # Dropdown Element + @dropdown = @wrap.find('.dropdown') + @dropdownContent = @dropdown.find('.dropdown-content') + + @locationBadgeEl = @getElement('.search-location-badge') + @locationText = @getElement('.location-text') + @scopeInputEl = @getElement('#scope') + @searchInput = @getElement('.search-input') + @projectInputEl = @getElement('#search_project_id') + @groupInputEl = @getElement('#group_id') + @searchCodeInputEl = @getElement('#search_code') + @repositoryInputEl = @getElement('#repository_ref') + @clearInput = @getElement('.js-clear-input') + + @saveOriginalState() + + # Only when user is logged in + @createAutocomplete() if gon.current_user_id + + @searchInput.addClass('disabled') + + @saveTextLength() + + @bindEvents() + + # Finds an element inside wrapper element + getElement: (selector) -> + @wrap.find(selector) + + saveOriginalState: -> + @originalState = @serializeState() + + saveTextLength: -> + @lastTextLength = @searchInput.val().length + + createAutocomplete: -> + @searchInput.glDropdown + filterInputBlur: false + filterable: true + filterRemote: true + highlight: true + enterCallback: false + filterInput: 'input#search' + search: + fields: ['text'] + data: @getData.bind(@) + selectable: true + clicked: @onClick.bind(@) + + getData: (term, callback) -> + _this = @ + + # Do not trigger request if input is empty + return if @searchInput.val() is '' + + # Prevent multiple ajax calls + return if @loadingSuggestions + + @loadingSuggestions = true + + jqXHR = $.get(@autocompletePath, { + project_id: @projectId + project_ref: @projectRef + term: term + }, (response) -> + # Hide dropdown menu if no suggestions returns + if !response.length + _this.disableAutocomplete() + return + + data = [] + + # List results + firstCategory = true + for suggestion in response + + # Add group header before list each group + if lastCategory isnt suggestion.category + data.push 'separator' if !firstCategory + + firstCategory = false if firstCategory + + data.push + header: suggestion.category + + lastCategory = suggestion.category + + data.push + id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}" + category: suggestion.category + text: suggestion.label + url: suggestion.url + + # Add option to proceed with the search + if data.length + data.push('separator') + data.push + text: "Result name contains \"#{term}\"" + url: "/search?\ + search=#{term}\ + &project_id=#{_this.projectInputEl.val()}\ + &group_id=#{_this.groupInputEl.val()}" + + callback(data) + ).always -> + _this.loadingSuggestions = false + + serializeState: -> + { + # Search Criteria + search_project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + scope: @scopeInputEl.val() + + # Location badge + _location: @locationText.text() + } + + bindEvents: -> + $(document).on 'click', @onDocumentClick + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'keyup', @onSearchInputKeyUp + @searchInput.on 'click', @onSearchInputClick + @searchInput.on 'focus', @onSearchInputFocus + @clearInput.on 'click', @onClearInputClick + + onDocumentClick: (e) => + # If clicking outside the search box + # And search input is not focused + # And we are not clicking inside a suggestion + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + @onSearchInputBlur() + + enableAutocomplete: -> + # No need to enable anything if user is not logged in + return if !gon.current_user_id + + _this = @ + @loadingSuggestions = false + + @dropdown.addClass('open') + @searchInput.removeClass('disabled') + + onSearchInputKeyDown: => + # Saves last length of the entered text + @saveTextLength() + + onSearchInputKeyUp: (e) => + switch e.keyCode + when KEYCODE.BACKSPACE + # when trying to remove the location badge + if @lastTextLength is 0 and @badgePresent() + @removeLocationBadge() + + # When removing the last character and no badge is present + if @lastTextLength is 1 + @disableAutocomplete() + + # When removing any character from existin value + if @lastTextLength > 1 + @enableAutocomplete() + + when KEYCODE.ESCAPE + @restoreOriginalState() + + else + # Handle the case when deleting the input value other than backspace + # e.g. Pressing ctrl + backspace or ctrl + x + if @searchInput.val() is '' + @disableAutocomplete() + else + # We should display the menu only when input is not empty + @enableAutocomplete() + + @wrap.toggleClass 'has-value', !!e.target.value + + # Avoid falsy value to be returned + return + + onSearchInputClick: (e) => + # Prevents closing the dropdown menu + e.stopImmediatePropagation() + + onSearchInputFocus: => + @isFocused = true + @wrap.addClass('search-active') + + onClearInputClick: (e) => + e.preventDefault() + @searchInput.val('').focus() + + onSearchInputBlur: (e) => + @isFocused = false + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = " + #{category}#{value} + " + @locationBadgeEl.html(html) + @wrap.addClass('has-location-badge') + + restoreOriginalState: -> + inputs = Object.keys @originalState + + for input in inputs + @getElement("##{input}").val(@originalState[input]) + + + if @originalState._location is '' + @locationBadgeEl.empty() + else + @addLocationBadge( + value: @originalState._location + ) + + @dropdown.removeClass 'open' + + badgePresent: -> + @locationBadgeEl.children().length + + resetSearchState: -> + inputs = Object.keys @originalState + + for input in inputs + + # _location isnt a input + break if input is '_location' + + @getElement("##{input}").val('') + + removeLocationBadge: -> + @locationBadgeEl.empty() + + # Reset state + @resetSearchState() + + @wrap.removeClass('has-location-badge') + + disableAutocomplete: -> + @searchInput.addClass('disabled') + @dropdown.removeClass('open') + @restoreMenu() + + restoreMenu: -> + html = "" + @dropdownContent.html(html) + + onClick: (item, $el, e) -> + if location.pathname.indexOf(item.url) isnt -1 + e.preventDefault() + if not @badgePresent + if item.category is 'Projects' + @projectInputEl.val(item.id) + @addLocationBadge( + value: 'This project' + ) + + if item.category is 'Groups' + @groupInputEl.val(item.id) + @addLocationBadge( + value: 'This group' + ) + + $el.removeClass('is-active') + @disableAutocomplete() + @searchInput.val('').focus() diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index eea3f5ee91..860d4f438d 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 084f0e0dc6..e4b7a3172e 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -10,10 +10,10 @@ class @Subscription btn = $(event.currentTarget) action = btn.find('span').text() current_status = @subscription_status.attr('data-status') - btn.prop('disabled', true) + btn.addClass('disabled') $.post @url, => - btn.prop('disabled', false) + btn.removeClass('disabled') status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed' @subscription_status.attr('data-status', status) action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index b6b4bd90e6..886da72e26 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -6,10 +6,12 @@ class @Todos clearListeners: -> $('.done-todo').off('click') $('.js-todos-mark-all').off('click') + $('.todo').off('click') initBtnListeners: -> $('.done-todo').on('click', @doneClicked) $('.js-todos-mark-all').on('click', @allDoneClicked) + $('.todo').on('click', @goToTodoUrl) doneClicked: (e) => e.preventDefault() @@ -54,3 +56,11 @@ class @Todos updateBadges: (data) -> $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + + goToTodoUrl: (e)-> + todoLink = $(this).data('url') + if e.metaKey + e.preventDefault() + window.open(todoLink,'_blank') + else + Turbolinks.visit(todoLink) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 3d6452d2f4..eee9b6e690 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,7 +1,9 @@ class @UsersSelect - constructor: -> + constructor: (currentUser) -> @usersPath = "/autocomplete/users.json" @userPath = "/autocomplete/users/:id.json" + if currentUser? + @currentUser = JSON.parse(currentUser) $('.js-user-search').each (i, dropdown) => $dropdown = $(dropdown) @@ -12,6 +14,81 @@ class @UsersSelect firstUser = $dropdown.data('first-user') selectedId = $dropdown.data('selected') defaultLabel = $dropdown.data('default-label') + issueURL = $dropdown.data('issueUpdate') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + abilityName = $dropdown.data('ability-name') + $value = $block.find('.value') + $collapsedSidebar = $block.find('.sidebar-collapsed-user') + $loading = $block.find('.block-loading').fadeOut() + + $block.on('click', '.js-assign-yourself', (e) => + e.preventDefault() + assignTo(@currentUser.id) + ) + + assignTo = (selected) -> + data = {} + data[abilityName] = {} + data[abilityName].assignee_id = selected + $loading + .fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + dataType: 'json' + url: issueURL + data: data + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $loading.fadeOut() + $selectbox.hide() + + if data.assignee + user = + name: data.assignee.name + username: data.assignee.username + avatar: data.assignee.avatar_url + else + user = + name: 'Unassigned' + username: '' + avatar: '' + $value.html(assigneeTemplate(user)) + $collapsedSidebar.html(collapsedAssigneeTemplate(user)) + + + collapsedAssigneeTemplate = _.template( + '<% if( avatar ) { %> + + + Toni Boehm + + <% } else { %> + + <% } %>' + ) + + assigneeTemplate = _.template( + '<% if (username) { %> + + <% if( avatar ) { %> + + <% } %> + <%= name %> + + @<%= username %> + + + <% } else { %> + + No assignee - + + assign yourself + + + <% } %>' + ) $dropdown.glDropdown( data: (term, callback) => @@ -30,6 +107,7 @@ class @UsersSelect if showNullUser showDivider += 1 users.unshift( + beforeDivider: true name: 'Unassigned', id: 0 ) @@ -39,6 +117,7 @@ class @UsersSelect name = showAnyUser name = 'Any User' if name == true anyUser = { + beforeDivider: true name: name, id: null } @@ -55,40 +134,73 @@ class @UsersSelect fields: ['name', 'username'] selectable: true fieldName: $dropdown.data('field-name') + toggleLabel: (selected) -> if selected && 'id' of selected selected.name else defaultLabel - clicked: -> + + inputId: 'issue_assignee_id' + + hidden: (e) -> + $selectbox.hide() + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + + clicked: (user) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is page is 'projects:merge_requests:index' + if $dropdown.hasClass('js-filter-bulk-update') + return if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedId = user.id Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']").val() + assignTo(selected) + renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false selected = if user.id is selectedId then "is-active" else "" img = "" - if avatar - img = "" + if user.beforeDivider? + "
  • + + #{user.name} + +
  • " + else + if avatar + img = "" - "
  • + # split into three parts so we can remove the username section if nessesary + listWithName = "
  • #{img} #{user.name} - - + " + + listWithUserName = " #{username} - - + " + listClosingTags = "
  • " + + + if username is '' + listWithUserName = '' + + listWithName + listWithUserName + listClosingTags ) $('.ajax-users-select').each (i, select) => diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index e1c5446eaa..99f35ecfb0 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -42,7 +42,7 @@ class @ZenMode $(e.currentTarget).trigger('zen_mode:leave') $(document).on 'zen_mode:enter', (e) => - @enter(e.target.parentNode) + @enter($(e.target).closest('.md-area').find('.zen-backdrop')) $(document).on 'zen_mode:leave', (e) => @exit() diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2d301d21ab..69b3b6586d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -9,6 +9,8 @@ *= require_self *= require dropzone/basic *= require cal-heatmap + *= require cropper.css + *= require animate */ /* diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 469f4f296a..542a53f037 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -13,10 +13,10 @@ // Toggle between two states. .js-toggler-container { - .turn-on { display: block; } + .turn-on { display: block; } .turn-off { display: none; } &.on { - .turn-on { display: none; } + .turn-on { display: none; } .turn-off { display: block; } } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 657c5f033c..e8c0172680 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -7,6 +7,7 @@ &:focus, &:active { outline: none; + background-color: $btn-active-gray; @include box-shadow($gl-btn-active-background); } } @@ -27,7 +28,8 @@ color: $color; } - &:active { + &:active, + &.active { @include box-shadow ($gl-btn-active-background); background-color: $dark; @@ -61,7 +63,7 @@ } @mixin btn-white { - @include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); } .btn { @@ -218,3 +220,26 @@ margin-right: 5px; } } + +.btn-text-field { + width: 100%; + text-align: left; + padding: 6px 16px; + border-color: $border-color; + color: $btn-placeholder-gray; + background-color: $background-color; + + &:hover, + &:active, + &:focus { + cursor: text; + box-shadow: none; + border-color: $border-color; + color: $btn-placeholder-gray; + background-color: $background-color; + } +} + +.btn-file-option { + background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); +} diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index e3192823a1..0b3af592d4 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,3 +1,9 @@ +.calender-block { + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + overflow-x: scroll; + } +} + .user-calendar-activities { .calendar_onclick_hr { padding: 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 8625817fda..2ade341c9d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -121,17 +121,10 @@ p.time { text-shadow: none; } -.thin_area{ +.thin_area { height: 150px; } -// Fixes alignment on notes. -.new_note { - label { - text-align: left; - } -} - // Fix issue with notes & lists creating a bunch of bottom borders. li.note { img { max-width: 100% } @@ -148,7 +141,7 @@ li.note { } } -.wiki_content code, .readme code{ +.wiki_content code, .readme code { background-color: inherit; } @@ -378,6 +371,7 @@ table { position: absolute; top: 0; right: 0; + min-width: 250px; visibility: hidden; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d92cf6e6c4..ba6c7930cd 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,7 +42,7 @@ font-size: 15px; text-align: left; border: 1px solid $dropdown-toggle-border-color; - border-radius: 2px; + border-radius: $dropdown-border-radius; outline: 0; text-overflow: ellipsis; white-space: nowrap; @@ -75,12 +75,12 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - padding: 10px; - font-size: 14px; + font-size: 15px; font-weight: normal; + padding: 10px 0; background-color: $dropdown-bg; border: 1px solid $dropdown-border-color; - border-radius: $border-radius-base; + border-radius: $dropdown-border-radius; box-shadow: 0 2px 4px $dropdown-shadow-color; &.is-loading { @@ -101,9 +101,17 @@ li { text-align: left; list-style: none; + padding: 0 10px; } .divider { + height: 1px; + margin: 8px 10px; + padding: 0; + background-color: $dropdown-divider-color; + } + + .separator { width: 100%; height: 1px; margin-top: 8px; @@ -130,6 +138,27 @@ text-decoration: none; outline: 0; } + + &.dropdown-menu-empty-link { + &.is-focused { + background-color: $dropdown-empty-row-bg; + } + } + + &.dropdown-menu-user-link { + line-height: 16px; + } + } + + .dropdown-header { + color: $dropdown-header-color; + font-size: 13px; + line-height: 22px; + padding: 0 10px 10px; + } + + .separator + .dropdown-header { + padding-top: 2px; } } @@ -148,6 +177,10 @@ .dropdown-menu-back { display: block; } + + .dropdown-content { + padding: 0 10px; + } } } @@ -161,13 +194,13 @@ } .dropdown-menu-user-link { - padding-top: 7px; + padding-top: 10px; padding-bottom: 7px; } .dropdown-menu-user-full-name { display: block; - font-weight: 600; + font-weight: 500; line-height: 16px; text-overflow: ellipsis; overflow: hidden; @@ -183,7 +216,7 @@ } .dropdown-select { - width: 280px; + width: $dropdown-width; } .dropdown-menu-align-right { @@ -212,20 +245,11 @@ } } -.dropdown-header { - padding-left: 5px; - padding-right: 5px; - color: $dropdown-header-color; - font-size: 13px; - line-height: 22px; -} .dropdown-title { position: relative; - margin-bottom: 10px; - padding-left: 30px; - padding-right: 30px; - padding-bottom: 10px; + padding: 0 25px 15px; + margin: 0 10px 10px; font-weight: 600; line-height: 1; text-align: center; @@ -237,7 +261,7 @@ .dropdown-title-button { position: absolute; - top: -1px; + top: 0; padding: 0; color: $dropdown-title-btn-color; font-size: 14px; @@ -251,25 +275,49 @@ } .dropdown-menu-close { - right: 0; + right: 5px; + width: 20px; + height: 20px; + top: -1px; } .dropdown-menu-back { - left: 0; + left: 7px; + top: 2px; } .dropdown-input { position: relative; margin-bottom: 10px; + padding: 0 10px; .fa { position: absolute; top: 10px; - right: 10px; + right: 20px; color: #c7c7c7; font-size: 12px; pointer-events: none; } + + .dropdown-input-clear { + display: none; + cursor: pointer; + pointer-events: all; + right: 22px; + top: 9px; + font-size: 14px; + } + + &.has-value { + .dropdown-input-clear { + display: block; + } + + .dropdown-input-search { + display: none; + } + } } .dropdown-input-field { @@ -286,13 +334,13 @@ border-color: $dropdown-input-focus-border; box-shadow: 0 0 4px $dropdown-input-focus-shadow; - + .fa { + ~ .fa { color: $dropdown-link-color; } } &:hover { - + .fa { + ~ .fa { color: $dropdown-link-color; } } @@ -338,11 +386,12 @@ } } -.dropdown-menu-labels { - .label { - position: relative; - width: 30px; - margin-right: 5px; - text-indent: -99999px; - } +.dropdown-label-box { + position: relative; + top: 3px; + margin-right: 5px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: $border-radius-base; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 646e261083..789df42fb6 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -3,12 +3,10 @@ * */ .file-holder { - border: none; border: 1px solid $border-color; &.readme-holder { - margin-top: 10px; - border-bottom: 0; + margin: $gl-padding-top 0; } table { @@ -17,11 +15,13 @@ .file-title { position: relative; - background: $background-color; + background-color: $background-color; border-bottom: 1px solid $border-color; margin: 0; text-align: left; padding: 10px $gl-padding; + word-wrap: break-word; + border-radius: 3px 3px 0 0; .file-actions { float: right; @@ -50,6 +50,10 @@ } } + a:not(.btn) { + color: $gl-dark-link-color; + } + .left-options { margin-top: -3px; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b05c5df1bd..9209347f9b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -3,7 +3,7 @@ vertical-align: top; } -@media (min-width: $screen-sm-min) { +@media (min-width: $screen-sm-min) { .issues-filters, .issues_bulk_update { .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4cb4129b71..54cb546111 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -6,40 +6,6 @@ input { border-radius: $border-radius-base; } -input[type='search'] { - background-color: white; - padding-left: 10px; -} - -input[type='search'].search-input { - background-repeat: no-repeat; - background-position: 10px; - background-size: 16px; - background-position-x: 30%; - padding-left: 10px; - background-color: $gray-light; - - &.search-input[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); - } - - &.search-input::-webkit-input-placeholder { - text-align: center; - } - - &.search-input:-moz-placeholder { /* Firefox 18- */ - text-align: center; - } - - &.search-input::-moz-placeholder { /* Firefox 19+ */ - text-align: center; - } - - &.search-input:-ms-input-placeholder { - text-align: center; - } -} - input[type='text'].danger { background: #f2dede!important; border-color: #d66; @@ -125,7 +91,7 @@ label { } .form-control::-webkit-input-placeholder { - color: #7f8fa4; + color: $gl-placeholder-color; } .input-group { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 6a68bb5c11..b3397d1601 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -36,7 +36,7 @@ header { padding: 0; .nav > li > a { - color: #7f8fa4; + color: $gl-icon-color; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -62,7 +62,7 @@ header { background-color: #eee; } &.active { - color: #7f8fa4; + color: $gl-icon-color; } } } @@ -81,14 +81,14 @@ header { font-size: 19px; line-height: $header-height; font-weight: normal; - color: #4c4e54; + color: $gl-text-color; overflow: hidden; text-overflow: ellipsis; vertical-align: top; white-space: nowrap; a { - color: #4c4e54; + color: $gl-text-color; &:hover { text-decoration: underline; } @@ -117,26 +117,6 @@ header { } } - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: ($header-height - 36) / 2; - - form { - margin: 0; - padding: 0; - } - - .search-input { - width: 220px; - - &:focus { - @include box-shadow(none); - outline: none; - } - } - } - .impersonation i { color: $red-normal; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 8328aac4e7..c8f86d60e3 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,9 +1,7 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - padding: 0; - border: 0; - margin-bottom: 5px; + margin-bottom: -5px; .div-dropzone-focus { border-color: #66afe9 !important; @@ -25,12 +23,10 @@ .div-dropzone-spinner { position: absolute; - top: 100%; - left: 100%; - margin-top: -1.1em; - margin-left: -1.1em; + bottom: 10px; + right: 5px; opacity: 0; - font-size: 30px; + font-size: 20px; transition: opacity 200ms ease-in-out; } @@ -65,17 +61,30 @@ position: relative; } +.md-header { + .nav-links { + .active { + a { + border-bottom-color: #000; + } + } + + a { + padding-top: 0; + line-height: 1; + } + } +} + .referenced-users { color: #4c4e54; padding-top: 10px; } .md-preview-holder { - background: #fff; - border: 1px solid #ddd; - min-height: 169px; - padding: 5px; - box-shadow: none; + min-height: 167px; + padding: 10px 0; + overflow-x: auto; } .markdown-area { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 5ea4f9a49d..66180f38a4 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -107,7 +107,7 @@ } .page-title { - .note_created_ago, .new-issue-link { + .note-created-ago, .new-issue-link { display: none; } } @@ -116,7 +116,7 @@ display: none; } - aside:not(.right-sidebar){ + aside:not(.right-sidebar) { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 95bdd6d1ea..94f5a12ff6 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -56,6 +56,17 @@ } } + .nav-search { + display: inline-block; + width: 50%; + padding: 11px 0; + + /* Small devices (phones, tablets, 768px and lower) */ + @media (max-width: $screen-sm-min) { + width: 100%; + } + } + .nav-links { display: inline-block; width: 50%; @@ -100,6 +111,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +122,7 @@ height: 34px; display: inline-block; position: relative; - top: 1px; + top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index fa7944cdab..b2fab387e1 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -44,13 +44,14 @@ @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0); @include border-radius ($border-radius-default); border: none; + min-width: 175px; } .select2-results .select2-result-label { padding: 10px 15px; } -.select2-drop{ +.select2-drop { color: #7f8fa4; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 6107c8a6d0..18189e985c 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,3 +1,10 @@ +#logo { + z-index: 2; + position: absolute; + width: 58px; + cursor: pointer; +} + .page-with-sidebar { padding-top: $header-height; transition-duration: .3s; @@ -18,24 +25,6 @@ position: absolute; left: 0; } - - #logo { - z-index: 2; - position: absolute; - width: 58px; - cursor: pointer; - } - - &.right-sidebar-expanded { - /* Extra small devices (phones, less than 768px) */ - /* No media query since this is the default in Bootstrap */ - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - padding-right: $gutter_width; - } - - } } .sidebar-wrapper { @@ -202,53 +191,27 @@ } } -@mixin expanded-sidebar { - padding-left: $sidebar_collapsed_width; +.collapse-nav a { + width: $sidebar_width; + position: fixed; + bottom: 0; + left: 0; + font-size: 13px; + background: transparent; + height: 40px; + text-align: center; + line-height: 40px; + transition-duration: .3s; + outline: none; - @media (min-width: $screen-md-min) { - padding-left: $sidebar_width; - } - - &.right-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - padding-right: $sidebar_collapsed_width; - } - } - - .sidebar-wrapper { - width: $sidebar_width; - - .nav-sidebar { - width: $sidebar_width; - } - - .nav-sidebar li a{ - width: 230px; - - &.back-link { - i { - opacity: 0; - } - } - } + &:hover { + text-decoration: none; } } -@mixin collapsed-sidebar { +.page-sidebar-collapsed { padding-left: $sidebar_collapsed_width; - &.right-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - padding-right: $sidebar_collapsed_width; - } - } - .sidebar-wrapper { width: $sidebar_collapsed_width; @@ -293,35 +256,56 @@ } } -.collapse-nav a { - width: $sidebar_width; - position: fixed; - bottom: 0; - left: 0; - font-size: 13px; - background: transparent; - height: 40px; - text-align: center; - line-height: 40px; - transition-duration: .3s; - outline: none; -} +.page-sidebar-expanded { + padding-left: $sidebar_collapsed_width; -.collapse-nav a:hover { - text-decoration: none; - background: #f2f6f7; -} + @media (min-width: $screen-md-min) { + padding-left: $sidebar_width; + } -.page-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ - @include collapsed-sidebar; - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - @include collapsed-sidebar; + .sidebar-wrapper { + width: $sidebar_width; + + .nav-sidebar { + width: $sidebar_width; + } + + .nav-sidebar li a { + width: 230px; + + &.back-link { + i { + opacity: 0; + } + } + } } } -.page-sidebar-expanded { - @include expanded-sidebar; +.right-sidebar-collapsed { + padding-right: 0; + + @media (min-width: $screen-sm-min) { + padding-right: $sidebar_collapsed_width; + } + + .sidebar-collapsed-icon { + cursor: pointer; + } +} + +.right-sidebar-expanded { + padding-right: 0; + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + padding-right: $sidebar_collapsed_width; + } + + @media (min-width: $screen-md-min) { + padding-right: $gutter_width; + } + + &.with-overlay { + padding-right: $sidebar_collapsed_width; + } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index aa244fe548..b91f2f6f89 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -14,10 +14,6 @@ background: $row-hover; } - &:last-child { - border-bottom: none; - } - .avatar { margin-right: 15px; } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index f63ac03323..c72af5dad0 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -56,8 +56,8 @@ $component-active-bg: $brand-info; //## $input-color: $text-color; -$input-border: #e7e9ed; -$input-border-focus: #7f8fa4; +$input-border: $border-color; +$input-border-focus: $focus-border-color; $legend-color: $text-color; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b1886fbe67..7b2aada5a0 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -138,6 +138,12 @@ } } + a.no-attachment-icon { + &:before { + display: none; + } + } + /* Link to current header. */ h1, h2, h3, h4, h5, h6 { position: relative; @@ -244,7 +250,7 @@ a > code { * Textareas intended for GFM * */ -textarea.js-gfm-input { +.js-gfm-input { font-family: $monospace_font; color: $gl-text-color; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index be626678bd..1ebbd9b0e5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,9 +10,10 @@ $gutter_inner_width: 258px; /* * UI elements */ -$border-color: #efeff1; +$border-color: #e5e5e5; +$focus-border-color: #3aabf0; $table-border-color: #eef0f2; -$background-color: #faf9f9; +$background-color: #fafafa; /* * Text @@ -26,6 +27,7 @@ $gl-text-orange: #d90; $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; +$gl-icon-color: $gl-placeholder-color; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -66,7 +68,7 @@ $header-height: 58px; $fixed-layout-width: 1280px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; -$border-radius-default: 3px; +$border-radius-default: 2px; $btn-transparent-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f; $ssh-key-icon-size: 18px; @@ -79,7 +81,7 @@ $provider-btn-not-active-color: #4688f1; $white-light: #fff; $white-normal: #ededed; -$white-dark: #ededed; +$white-dark: #ececec; $gray-light: #faf9f9; $gray-normal: #f5f5f5; @@ -102,9 +104,11 @@ $orange-light: rgba(252, 109, 38, 0.80); $orange-normal: #e75e40; $orange-dark: #ce5237; -$red-light: #f06559; -$red-normal: #e52c5a; -$red-dark: #d22852; +$red-light: #e52c5a; +$red-normal: #d22852; +$red-dark: darken($red-normal, 5%); + +$black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; @@ -126,9 +130,9 @@ $border-orange-light: #fc6d26; $border-orange-normal: #ce5237; $border-orange-dark: #c14e35; -$border-red-light: #f24f41; -$border-red-normal: #d22852; -$border-red-dark: #ca264f; +$border-red-light: #d22852; +$border-red-normal: #ca264f; +$border-red-dark: darken($border-red-normal, 5%); $help-well-bg: #fafafa; $help-well-border: #e5e5e5; @@ -148,15 +152,22 @@ $gl-success: $green-normal; $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; -$gl-btn-active-background: rgba(0, 0, 0, 0.12); -$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background; +$gl-btn-active-background: rgba(0, 0, 0, 0.16); +$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* * Commit Diff Colors */ $added: #63c363; $deleted: #f77; - +$line-added: #ecfdf0; +$line-added-dark: #c7f0d2; +$line-removed: #fbe9eb; +$line-removed-dark: #fac5cd; +$line-number-old: #f9d7dc; +$line-number-new: #ddfbe6; +$match-line: #fafafa; +$table-border-gray: #f0f0f0; /* * Fonts */ @@ -166,17 +177,20 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif /* * Dropdowns */ +$dropdown-border-radius: 2px; +$dropdown-width: 300px; $dropdown-bg: #fff; $dropdown-link-color: #555; -$dropdown-link-hover-bg: rgba(#000, .04); +$dropdown-link-hover-bg: $row-hover; +$dropdown-empty-row-bg: rgba(#000, .04); $dropdown-border-color: rgba(#000, .1); $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; -$dropdown-input-color: #c7c7c7; -$dropdown-input-focus-border: rgb(58, 171, 240); -$dropdown-input-focus-shadow: rgba(#000, .2); +$dropdown-input-color: #555; +$dropdown-input-focus-border: $focus-border-color; +$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-toggle-bg: #fff; @@ -186,9 +200,42 @@ $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); $dropdown-toggle-icon-color: #c4c4c4; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; +/* +* Buttons +*/ +$btn-active-gray: #ececec; +$btn-placeholder-gray: #c7c7c7; +$btn-white-active: #848484; + /* * Award emoji */ $award-emoji-menu-bg: #fff; $award-emoji-menu-border: #f1f2f4; $award-emoji-new-btn-icon-color: #dcdcdc; + +/* + * Search Box + */ +$search-input-border-color: rgba(#4688f1, .8); +$search-input-focus-shadow-color: $dropdown-input-focus-shadow; +$search-input-width: 244px; +$location-badge-color: #aaa; +$location-badge-bg: $gray-normal; +$location-badge-active-bg: #4f91f8; +$location-icon-color: #e7e9ed; +$location-icon-active-color: #807e7e; + +/* + * Notes + */ +$notes-light-color: #8e8e8e; +$notes-action-color: #c3c3c3; +$notes-role-color: #8e8e8e; +$notes-role-border-color: #e4e4e4; + +$note-disabled-comment-color: #b2b2b2; +$note-form-border-color: #e5e5e5; +$note-toolbar-color: #959494; + +$zen-control-hover-color: #111; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 02e24ec7c4..f870ea0d87 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,61 +1,62 @@ -.zennable { - a.js-zen-enter { - color: $gl-gray; - position: absolute; +.zen-backdrop { + &.fullscreen { + background-color: white; + position: fixed; top: 0; - right: 4px; - line-height: 56px; - } + bottom: 0; + left: 0; + right: 0; + z-index: 1031; - a.js-zen-leave { - display: none; - color: $gl-text-color; - position: absolute; - top: 10px; - right: 10px; - padding: 5px; - font-size: 36px; - - &:hover { - color: #111; + textarea { + border: none; + box-shadow: none; + border-radius: 0; + color: #000; + font-size: 20px; + line-height: 26px; + padding: 30px; + display: block; + outline: none; + resize: none; + height: 100vh; + max-width: 900px; + margin: 0 auto; } - } - .zen-backdrop { - &.fullscreen { - background-color: white; - position: fixed; + .zen-control-leave { + display: block; + position: absolute; top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1031; - - textarea { - border: none; - box-shadow: none; - border-radius: 0; - color: #000; - font-size: 20px; - line-height: 26px; - padding: 30px; - display: block; - outline: none; - resize: none; - height: 100vh; - max-width: 900px; - margin: 0 auto; - } - - a.js-zen-enter { - display: none; - } - - a.js-zen-leave { - display: block; - position: absolute; - top: 0; - } } } } + +.zen-cotrol { + padding: 0; + color: #555; + background: none; + border: 0; +} + +.zen-control-full { + color: $note-toolbar-color; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } +} + +.zen-control-leave { + display: none; + color: $gl-text-color; + position: absolute; + right: 10px; + padding: 5px; + font-size: 36px; + + &:hover { + color: $zen-control-hover-color; + } +} diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index b90c95c62d..c482a1258f 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -6,7 +6,7 @@ } .diff-line-num, .diff-line-num a { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; } // Code itself @@ -30,7 +30,7 @@ } .line_content.match { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; background: rgba(255, 255, 255, 0.4); } } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 8c1b0cd84e..28331f5975 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -6,12 +6,12 @@ } .diff-line-num, .diff-line-num a { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; } // Code itself pre.code, .diff-line-num { - border-color: $border-color; + border-color: $table-border-gray; } &, pre.code, .line_holder .line_content { @@ -23,36 +23,36 @@ .line_holder { .diff-line-num { &.old { - background: #fdd; - border-color: #f1c0c0; + background-color: $line-number-old; + border-color: $line-removed-dark; } &.new { - background: #dbffdb; - border-color: #c1e9c1; + background-color: $line-number-new; + border-color: $line-added-dark; } } .line_content { &.old { - background: #ffecec; + background: $line-removed; span.idiff { - background-color: #f8cbcb; + background-color: $line-removed-dark; } } &.new { - background: #eaffea; + background-color: $line-added; span.idiff { - background-color: #a6f3a6; + background-color: $line-added-dark; } } &.match { - color: rgba(0, 0, 0, 0.3); - background: #fafafa; + color: $black-transparent; + background: $match-line; } } } diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 28994e60ba..37bf38fa65 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -37,7 +37,7 @@ height: 300px; overflow-y: scroll; - input.emoji-search{ + input.emoji-search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); background-repeat: no-repeat; background-position: right 5px center; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 2a7b5cfc7f..67a9d7d2cf 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -42,7 +42,7 @@ } } - .loading{ + .loading { font-size: 20px; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 971656feb4..358d2f4ab9 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -1,15 +1,15 @@ -.commit-title{ +.commit-title { display: block; } -.commit-author, .commit-committer{ +.commit-author, .commit-committer { display: block; color: #999; font-weight: normal; font-style: italic; } -.commit-author strong, .commit-committer strong{ +.commit-author strong, .commit-committer strong { font-weight: bold; font-style: normal; } @@ -20,6 +20,8 @@ margin: 0; padding: 0; margin-top: 10px; + word-break: normal; + white-space: pre-wrap; } .commit-info-row { @@ -74,7 +76,7 @@ color: $gl-text-red; } } - .edit-file{ + .edit-file { a { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 33b3c7558e..6453c91d95 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,4 +1,4 @@ -.commits-compare-switch{ +.commits-compare-switch { @include btn-default; @include btn-white; background: image-url("switch_icon.png") no-repeat center center; @@ -47,6 +47,7 @@ li.commit { .commit_short_id { min-width: 65px; + color: $gl-dark-link-color; font-family: $monospace_font; } @@ -88,17 +89,24 @@ li.commit { padding: 0; margin: 0; } + + a { + color: $gl-dark-link-color; + } } .commit-row-info { color: $gl-gray; line-height: 24px; - font-size: 13px; a { color: $gl-gray; } + .avatar { + margin-right: 8px; + } + .committed_ago { display: inline-block; } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index d3eda1a57e..5917f08972 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -33,8 +33,12 @@ .description { margin-top: 6px; - p:last-child { - margin-bottom: 0; + p { + overflow-x: auto; + + &:last-child { + margin-bottom: 0; + } } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f1368d74b3..d0855f6691 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -2,6 +2,7 @@ .diff-file { border: 1px solid $border-color; margin-bottom: $gl-padding; + border-radius: 3px; .diff-header { position: relative; @@ -10,6 +11,7 @@ padding: 10px 16px; color: #555; z-index: 10; + border-radius: 3px 3px 0 0; .diff-title { font-family: $monospace_font; @@ -31,6 +33,7 @@ overflow-y: hidden; background: #fff; color: #333; + border-radius: 0 0 3px 3px; .unfold { cursor: pointer; @@ -59,9 +62,14 @@ border-collapse: separate; margin: 0; padding: 0; + .line_holder td { line-height: $code_line_height; font-size: $code_font_size; + + span { + white-space: pre; + } } } @@ -104,6 +112,10 @@ display: table-cell; } } + + .text-file.diff-wrap-lines table .line_holder td span { + white-space: pre-wrap; + } } .image { background: #ddd; @@ -316,6 +328,16 @@ float: right; } +.diffs { + .content-block { + border-bottom: none; + } +} + +.files-changed { + border-bottom: none; +} + // Mobile @media (max-width: 480px) { .diff-title { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 43be5e38ba..0f0592a0ab 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,5 +1,5 @@ .file-editor { - #editor{ + #editor { border: none; @include border-radius(0); height: 500px; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 84eefd01cf..c66efe978c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -43,10 +43,6 @@ .md { color: #7f8fa4; font-size: $gl-font-size; - - iframe.twitter-share-button { - vertical-align: bottom; - } } pre { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index bd224705f0..604f1700cf 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,6 +59,9 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { + margin: -$gl-padding+1; + } } body.modal-open { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5300bb52a1..88c1b614c7 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -30,6 +30,10 @@ } .issuable-sidebar { + a { + color: inherit; + } + .block { @include clearfix; padding: $gl-padding 0; @@ -89,7 +93,7 @@ } .cross-project-reference { - color: $gl-link-color; + color: inherit; span { white-space: nowrap; @@ -133,6 +137,12 @@ .value { line-height: 1; + + .assign-yourself { + margin-top: 10px; + font-weight: normal; + display: block; + } } .bold { @@ -252,6 +262,15 @@ text-decoration: none; } } + + .dropdown-menu-toggle { + width: 100%; + padding-top: 6px; + } + + .open .dropdown-menu { + width: 100%; + } } .btn-default.gutter-toggle { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3c13573c8f..3e0a3140be 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -9,29 +9,55 @@ } &.suggest-colors-dropdown { - margin-bottom: 5px; + margin-top: 10px; + margin-bottom: 10px; + border-radius: $border-radius-base; + overflow: hidden; a { @include border-radius(0); - width: 36.7px; + width: (100% / 7); margin-right: 0; margin-bottom: -5px; } } } -.dropdown-label-color-preview { - display: none; - margin-top: 5px; - width: 100%; - height: 25px; - - &.is-active { - display: block; +.dropdown-new-label { + .dropdown-content { + max-height: 260px; } } +.dropdown-label-color-input { + position: relative; + margin-bottom: 10px; + + &.is-active { + padding-left: 32px; + } +} + +.dropdown-label-color-preview { + position: absolute; + left: 0; + top: 0; + width: 32px; + height: 32px; + border-top-left-radius: $border-radius-base; + border-bottom-left-radius: $border-radius-base; +} + .label-row { + .label-name { + display: inline-block; + width: 200px; + + @media (max-width: $screen-xs-min) { + display: block; + } + } + .label { padding: 9px; font-size: 14px; @@ -52,3 +78,52 @@ background-color: $gl-danger; color: $white-light; } + +.manage-labels-list { + + .prepend-left-10 { + display: inline-block; + width: 40%; + vertical-align: middle; + + @media (max-width: $screen-xs-min) { + display: block; + width: 100%; + margin-left: 0; + padding: 10px 0; + } + } + + .pull-info-right { + float: right; + + @media (max-width: $screen-xs-min) { + float: none; + } + + .action-buttons { + border-color: transparent; + padding: 6px; + color: $gl-text-color; + + &.subscribe-button { + padding-left: 0; + } + } + + i { + color: $gl-text-color; + } + + .append-right-20 { + a { + color: $gl-text-color; + } + + @media (max-width: $screen-xs-min) { + display: block; + margin-bottom: 10px; + } + } + } +} diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6d2bd33b28..6926448519 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,9 +1,9 @@ .ci-body { - .incorrect-syntax{ + .incorrect-syntax { font-size: 19px; color: red; } - .correct-syntax{ + .correct-syntax { font-size: 19px; color: #47a447; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 777bcbca5c..403171d453 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -36,7 +36,7 @@ } } - .login-box{ + .login-box { background: #fafafa; border-radius: 10px; box-shadow: 0 0 2px #ccc; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index cee5c47cfb..b79335eab9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -123,6 +123,8 @@ .mr_source_commit, .mr_target_commit { + margin-bottom: 0; + .commit { margin: 0; padding: 2px 0; @@ -174,10 +176,6 @@ display: none; } -.merge-request-form .select2-container { - width: 250px !important; -} - #modal_merge_info .modal-dialog { width: 600px; @@ -195,38 +193,81 @@ line-height: 31px; } -.disabled-comment-area { - padding: 16px 0; - - .disabled-profile { - width: 40px; - height: 40px; - background: $border-gray-dark; - border-radius: 20px; - display: inline-block; - margin-right: 10px; - } - - .disabled-comment { - background: $gray-light; - display: inline-block; - vertical-align: top; - height: 200px; - border-radius: 4px; - border: 1px solid $border-gray-normal; - padding-top: 90px; - text-align: center; - right: 20px; - position: absolute; - left: 70px; - margin-bottom: 20px; - - span { - color: #b2b2b2; - - a { - color: $md-link-color; - } - } +.builds { + .table-holder { + overflow-x: scroll; } } + +.panel-new-merge-request { + .panel-heading { + padding: 5px 10px; + font-weight: 600; + line-height: 25px; + } + + .panel-body { + padding: 10px 5px; + } + + .panel-footer { + padding: 5px 10px; + } + + .commit { + .commit-row-title { + margin-bottom: 4px; + } + + .avatar { + width: 20px; + height: 20px; + margin-right: 5px; + } + + .commit-row-info { + line-height: 20px; + } + } + + .btn-clipboard { + margin-right: 5px; + padding: 0; + background: transparent; + } + + .ci-status-link { + margin-right: 5px; + } +} + +.merge-request-select { + padding-left: 5px; + padding-right: 5px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + @media (min-width: $screen-sm-min) { + float: left; + width: 50%; + margin-bottom: 0; + } + + .dropdown-menu-toggle { + width: 100%; + } + + .dropdown-menu { + left: 5px; + right: 5px; + width: auto; + } +} + +.issuable-form-select-holder { + display: inline-block; + width: 250px; +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index daf2651425..4d4d508396 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -1,14 +1,10 @@ /** * Note Form */ - .comment-btn { @extend .btn-create; } -.reply-btn { - @extend .btn-primary; - margin: 10px $gl-padding; -} + .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { opacity: 1.0; @@ -17,16 +13,17 @@ } .diff-file, .discussion { - .new_note { + .new-note { margin: 0; border: none; } } -.new_note { + +.new-note { display: none; } -.new_note, .note-edit-form { +.new-note, .note-edit-form { .note-form-actions { margin-top: $gl-padding; } @@ -40,19 +37,16 @@ img { max-width: 100%; } - - .note_text { - width: 100%; - } - - .comment-hints { - margin-top: -12px; - } } -/* loading indicator */ -.notes-busy { - margin: 18px; +.note-textarea { + padding: 10px 0; + font-family: $regular_font; + border: 0; + + &:focus { + outline: 0; + } } .note-image-attach { @@ -62,40 +56,29 @@ } .common-note-form { - margin: 0; - background: #fff; - padding: $gl-padding; - margin-left: -$gl-padding; - margin-right: -$gl-padding; - margin-bottom: -$gl-padding; -} + .md-area { + padding: $gl-padding-top $gl-padding; + border: 1px solid $note-form-border-color; + border-radius: $border-radius-base; -.note-form-actions { - background: #fff; + &.is-focused { + border-color: $focus-border-color; + box-shadow: 0 0 2px rgba(#000, .2), + 0 0 4px rgba($focus-border-color, .4); - .note-form-option { - margin-top: 8px; - margin-left: 30px; - @extend .pull-left; - } - - .js-notify-commit-author { - float: left; - } - - .write-preview-btn { - // makes the "absolute" position for links relative to this - position: relative; - - // preview/edit buttons - > a { - position: absolute; - right: 5px; - top: 8px; + .comment-toolbar, + .nav-links { + border-color: $focus-border-color; + } } } } +.discussion-form { + padding: $gl-padding-top $gl-padding; + background-color: #fff; +} + .note-edit-form { display: none; font-size: 15px; @@ -130,13 +113,12 @@ .discussion-body, .diff-file { .notes .note { - border-color: #ddd; padding: 10px 15px; } .discussion-reply-holder { - background: $background-color; - border-top: 1px solid $border-color; + background-color: $white-light; + padding: 10px 16px; } } @@ -154,11 +136,49 @@ } } -.comment-hints { - color: #999; - background: #fff; - padding: 7px; - margin-top: -7px; - border: 1px solid $border-color; - font-size: 13px; +.comment-toolbar { + padding-top: $gl-padding-top; + color: $note-toolbar-color; + border-top: 1px solid $border-color; +} + +.toolbar-button { + padding: 0; + background: none; + border: 0; + font-size: 14px; + line-height: 16px; + + &:hover, + &:focus { + color: $gl-link-color; + outline: 0; + } + + @media (min-width: $screen-md-min) { + float: left; + margin-right: $gl-padding; + + &:last-child { + float: right; + margin-right: 0; + } + } +} + +.toolbar-button-icon { + position: relative; + top: 1px; + margin-right: 3px; + color: inherit; + font-size: 16px; +} + +.toolbar-text { + font-size: 14px; + line-height: 16px; + + @media (min-width: $screen-md-min) { + float: left; + } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4bd2016bdc..7295fe5112 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -20,9 +20,15 @@ ul.notes { .timeline-content { margin-left: 55px; + + &.timeline-content-form { + @media (max-width: $screen-sm-max) { + margin-left: 0; + } + } } - .note_created_ago, .note-updated-at { + .note-created-ago, .note-updated-at { white-space: nowrap; } @@ -39,53 +45,6 @@ ul.notes { } } - .discussion-header, - .note-header { - @extend .cgray; - - a:hover { - text-decoration: none; - } - - .avatar { - float: left; - margin-right: 10px; - } - - .discussion-last-update, - .note-last-update { - &:before { - content: "\00b7"; - } - - a { - color: $gl-gray; - - &:hover { - text-decoration: underline; - } - } - } - .author { - color: #4c4e54; - margin-right: 3px; - - &:hover { - color: $gl-link-color; - } - } - .author-username { - } - - .note-role { - float: right; - margin-top: 1px; - border: 1px solid #bbb; - background-color: transparent; - color: $gl-gray; - } - } - .discussion-body { padding-top: 15px; } @@ -99,6 +58,7 @@ ul.notes { .note { display: block; position: relative; + border-bottom: 1px solid $table-border-gray; &.is-editting { .note-header, @@ -123,7 +83,7 @@ ul.notes { // On diffs code should wrap nicely and not overflow pre { code { - white-space: pre-wrap; + white-space: pre; } } @@ -158,9 +118,6 @@ ul.notes { padding-bottom: 3px; } - &:last-child { - border-bottom: 1px solid $border-color; - } } } @@ -178,14 +135,14 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid #ddd; + border: 1px solid $table-border-gray; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: #fff; + background: $background-color; color: $text-color; } &.notes_line2 { @@ -196,42 +153,99 @@ ul.notes { &.notes_content { background-color: #fff; border-width: 1px 0; - padding-top: 0; + padding: 0; vertical-align: top; - &.parallel{ + &.parallel { border-width: 1px; } } } } +.discussion-header, +.note-header { + a { + color: inherit; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } + } + +} + +.note-headline-light, +.discussion-headline-light { + color: $notes-light-color; +} + /** * Actions for Discussions/Notes */ -.discussion, -.note { - .discussion-actions, - .note-actions { - float: right; - margin-left: 10px; +.discussion-actions, +.note-actions { + float: right; + margin-left: 10px; + color: $notes-action-color; +} - a { - margin-left: 5px; - color: $gl-gray; +.note-action-button, +.discussion-action-button { + display: inline-block; + margin-left: 10px; + line-height: 24px; - i.fa { - font-size: 16px; - line-height: 16px; - } + .fa { + color: $notes-action-color; + position: relative; + top: 1px; + font-size: 17px; + } + &.js-note-delete { + i { &:hover { - @extend .cgray; - &.danger { @extend .cred; } + color: $gl-text-red; + } + } + } + + &.js-note-edit { + i { + &:hover { + color: $gl-link-color; } } } } + +.discussion-toggle-button { + line-height: 20px; + font-size: 13px; + + .fa { + margin-right: 3px; + font-size: 10px; + line-height: 18px; + vertical-align: top; + } +} + +.note-role { + position: relative; + top: -2px; + display: inline-block; + padding-left: 4px; + padding-right: 4px; + color: $notes-role-color; + font-size: 12px; + line-height: 20px; + border: 1px solid $notes-role-border-color; + border-radius: $border-radius-base; +} + .diff-file .note .note-actions { right: 0; top: 0; @@ -280,3 +294,21 @@ ul.notes { } } } + +.disabled-comment { + margin-left: -$gl-padding-top; + margin-right: -$gl-padding-top; + background-color: $gray-light; + border-radius: $border-radius-base; + border: 1px solid $border-gray-normal; + color: $note-disabled-comment-color; + line-height: 200px; + + .disabled-comment-text { + line-height: normal; + } + + a { + color: $gl-link-color; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index e96dfc8e8d..a9656e5cae 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -197,3 +197,24 @@ width: 105px; } } + +.modal-profile-crop { + .modal-dialog { + width: 380px; + + @media (max-width: $screen-sm-min) { + width: auto; + } + + } + + .profile-crop-image-container { + height: 300px; + margin: 0 auto; + } + + .crop-controls { + padding: 10px 0 0; + text-align: center; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c68bd673a6..fcca9d4faf 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -162,7 +162,7 @@ margin-right: 12px; a { - margin: -1px !important; + margin: -1px; } } @@ -222,13 +222,17 @@ padding: 0; background: transparent; border: none; - line-height: 42px; + line-height: 36px; margin: 0; > li + li:before { padding: 0 3px; color: #999; } + + a { + color: $gl-dark-link-color; + } } .last-push-widget { @@ -311,7 +315,7 @@ pre.light-well { } .git-empty { - margin: 0 7px; + margin: 0 7px 7px; h5 { color: #5c5d5e; @@ -397,7 +401,7 @@ pre.light-well { } .commit_short_id { - margin-right: 5px; + margin: 0 5px; color: $gl-link-color; font-weight: 600; } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b6e4502464..f0f3744c6f 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,3 +21,145 @@ } } +.search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 35) / 2; + + form { + @extend .form-control; + margin: 0; + padding: 4px; + width: $search-input-width; + line-height: 24px; + } + + .location-text { + font-style: normal; + } + + .search-input { + border: none; + font-size: 14px; + outline: none; + padding: 0; + margin-left: 5px; + line-height: 25px; + width: 98%; + } + + .location-badge { + line-height: 25px; + padding: 0 5px; + border-radius: $border-radius-default; + font-size: 14px; + font-style: normal; + color: $location-badge-color; + display: inline-block; + background-color: $location-badge-bg; + vertical-align: top; + } + + .search-input-container { + display: -webkit-flex; + display: flex; + position: relative; + } + + .search-location-badge, .search-input-wrap { + // Fallback if flexbox is not supported + display: inline-block; + } + + .search-input-wrap { + width: 100%; + + .search-icon, .clear-icon { + position: absolute; + right: 5px; + top: 0; + color: $location-icon-color; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + .search-icon { + @extend .fa-search; + @include transition(color .15s); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + .clear-icon { + @extend .fa-times; + display: none; + } + + // Rewrite position. Dropdown menu should be relative to .search-input-container + .dropdown { + position: static; + } + + .dropdown-header { + text-transform: uppercase; + font-size: 11px; + } + + // Custom dropdown positioning + .dropdown-menu { + top: 30px; + left: -5px; + padding: 0; + + ul { + padding: 10px 0; + } + } + + .dropdown-content { + max-height: 350px; + } + } + + &.search-active { + form { + @extend .form-control:focus; + border-color: $dropdown-input-focus-border; + box-shadow: 0 0 4px $search-input-focus-shadow-color; + } + + .location-badge { + @include transition(all .15s); + background-color: $location-badge-active-bg; + color: $white-light; + } + + .search-input-wrap { + i { + color: $location-icon-active-color; + } + } + } + + &.has-value { + .search-icon { + display: none; + } + + .clear-icon { + cursor: pointer; + display: block; + } + } + + &.has-location-badge { + .search-input-wrap { + width: 78%; + } + } +} diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 6f777d1164..dbb6daf0d7 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,54 +1,58 @@ -.ci-status { - padding: 2px 7px; - margin-right: 5px; - border: 1px solid #eee; - white-space: nowrap; - @include border-radius(4px); +.container-fluid { + .ci-status { + padding: 2px 7px; + margin-right: 5px; + border: 1px solid #eee; + white-space: nowrap; + @include border-radius(4px); - &:hover { - text-decoration: none; + &:hover { + text-decoration: none; + } + + &.ci-failed { + color: $gl-danger; + border-color: $gl-danger; + } + + &.ci-success { + color: $gl-success; + border-color: $gl-success; + } + + &.ci-info { + color: $gl-info; + border-color: $gl-info; + } + + &.ci-canceled, + &.ci-skipped, + &.ci-disabled { + color: $gl-gray; + border-color: $gl-gray; + } + + &.ci-pending, + &.ci-running { + color: $gl-warning; + border-color: $gl-warning; + } } - &.ci-failed { - color: $gl-danger; - border-color: $gl-danger; - } - - &.ci-success { + .ci-status-icon-success { color: $gl-success; - border-color: $gl-success; } - - &.ci-info { - color: $gl-info; - border-color: $gl-info; + .ci-status-icon-failed { + color: $gl-danger; } - - &.ci-disabled { - color: $gl-gray; - border-color: $gl-gray; - } - - &.ci-pending, - &.ci-running { + .ci-status-icon-running, + .ci-status-icon-pending { color: $gl-warning; - border-color: $gl-warning; + } + .ci-status-icon-canceled, + .ci-status-icon-disabled, + .ci-status-icon-not-found, + .ci-status-icon-skipped { + color: $gl-gray; } } - -.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/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index f983e9829e..e83fa9e3d5 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -6,13 +6,19 @@ .navbar-nav { li { .badge.todos-pending-count { - background-color: #7f8fa4; + background-color: $gl-icon-color; margin-top: -5px; font-weight: normal; } } } +.todo { + &:hover { + cursor: pointer; + } +} + .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ed9f603138..f010436bd3 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :require_two_factor_authentication, :two_factor_grace_period, :gravatar_enabled, - :twitter_sharing_enabled, :sign_in_text, :help_page_text, :home_page_url, diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4089091d56..c6b3105544 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,7 +5,7 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? + @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.non_archived unless params[:with_archived].present? diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c81cb85dc1..97d53acde9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -47,6 +47,16 @@ class ApplicationController < ActionController::Base email: current_user.email, username: current_user.username, ) + + Raven.tags_context(program: sentry_program_context) + end + end + + def sentry_program_context + if Sidekiq.server? + 'sidekiq' + else + 'rails' end end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 081e01a75e..8bf71a1adb 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,11 +1,15 @@ module Ci class ProjectsController < Ci::ApplicationController before_action :project - before_action :authorize_read_project!, except: [:badge] before_action :no_cache, only: [:badge] + before_action :authorize_read_project!, except: [:badge, :index] skip_before_action :authenticate_user!, only: [:badge] protect_from_forgery + def index + redirect_to root_path + end + def show # Temporary compatibility with CI badges pointing to CI project page redirect_to namespace_project_path(project.namespace, project) @@ -35,5 +39,9 @@ module Ci response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end + + def authorize_read_project! + return access_denied! unless can?(current_user, :read_project, project) + end end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index b23c3022fb..9d5a28e8d4 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -18,14 +18,14 @@ class Groups::MilestonesController < Groups::ApplicationController end def create - project_ids = params[:milestone][:project_ids] + project_ids = params[:milestone][:project_ids].reject(&:blank?) title = milestone_params[:title] - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute + if create_milestones(project_ids) + redirect_to milestone_path(title) + else + render_new_with_error(project_ids.empty?) end - - redirect_to milestone_path(title) end def show @@ -41,6 +41,27 @@ class Groups::MilestonesController < Groups::ApplicationController private + def create_milestones(project_ids) + return false unless project_ids.present? + + ActiveRecord::Base.transaction do + @projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute + end + end + + true + rescue ActiveRecord::ActiveRecordError => e + flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}" + false + end + + def render_new_with_error(empty_project_ids) + @milestone = Milestone.new(milestone_params) + @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids + render :new + end + def authorize_admin_milestones! return render_404 unless can?(current_user, :admin_milestones, group) end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 21135f7d60..df98f56a1c 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -55,11 +55,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end else saml_user = Gitlab::Saml::User.new(oauth) - saml_user.save + saml_user.save if saml_user.changed? @user = saml_user.gl_user continue_login_process end + rescue Gitlab::OAuth::SignupDisabledError + handle_signup_error end def omniauth_error @@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end rescue Gitlab::OAuth::SignupDisabledError - label = Gitlab::OAuth::Provider.label_for(oauth['provider']) - message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." - - if current_application_settings.signup_enabled? - message << " Create a GitLab account first, and then connect it to your #{label} account." - end - - flash[:notice] = message - - redirect_to new_user_session_path + handle_signup_error end def handle_service_ticket provider, ticket @@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end + def handle_signup_error + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{label} account." + end + + flash[:notice] = message + + redirect_to new_user_session_path + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 9042d8e5f0..c5fa756d02 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController def update user_params.except!(:email) if @user.ldap_user? - if @user.update_attributes(user_params) - flash[:notice] = "Profile was successfully updated" - else - messages = @user.errors.full_messages.uniq.join('. ') - flash[:alert] = "Failed to update profile. #{messages}" - end - respond_to do |format| - format.html { redirect_back_or_default(default: { action: 'show' }) } + if @user.update_attributes(user_params) + message = "Profile was successfully updated" + format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) } + format.json { render json: { message: message } } + else + message = @user.errors.full_messages.uniq.join('. ') + format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) } + format.json { render json: { message: message }, status: :unprocessable_entity } + end end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 5f5dc1adad..275e94d39e 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -80,7 +80,9 @@ class Projects::ApplicationController < ApplicationController end def require_non_empty_project - redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo? + # Be sure to return status code 303 to avoid a double DELETE: + # http://api.rubyonrails.org/classes/ActionController/Redirecting.html + redirect_to namespace_project_path(@project.namespace, @project), status: 303 if @project.empty_repo? end def require_branch_head diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 6ff47c4033..824aa41db5 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -1,12 +1,20 @@ class Projects::BadgesController < Projects::ApplicationController - before_action :no_cache_headers + layout 'project_settings' + before_action :authorize_admin_project!, only: [:index] + before_action :no_cache_headers, except: [:index] + + def index + @ref = params[:ref] || @project.default_branch || 'master' + @build_badge = Gitlab::Badge::Build.new(@project, @ref) + end def build + badge = Gitlab::Badge::Build.new(project, params[:ref]) + respond_to do |format| format.html { render_404 } format.svg do - image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) - send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml') + send_data(badge.data, type: badge.type, disposition: 'inline') end end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index c0a5373492..d09e7375b6 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -48,7 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController respond_to do |format| format.html do redirect_to namespace_project_branches_path(@project.namespace, - @project) + @project), status: 303 end format.js { render status: status[:return_code] } end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 877b39c9b1..6d649e72f8 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController @merge_requests = @issue.referenced_merge_requests(current_user) @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) - respond_with(@issue) + respond_to do |format| + format.html + format.json do + render json: @issue.to_json(include: [:milestone, :labels]) + end + end + end def create @@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: { - saved: @issue.valid?, - assignee_avatar_url: @issue.assignee.try(:avatar_url) - } + render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b830d77775..3e0cfc6aa6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html format.json { render json: @merge_request } - format.diff { render text: @merge_request.to_diff(current_user) } - format.patch { render text: @merge_request.to_patch(current_user) } + format.diff { render text: @merge_request.to_diff } + format.patch { render text: @merge_request.to_patch } end end @@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.target_project, @merge_request]) end format.json do - render json: { - saved: @merge_request.valid?, - assignee_avatar_url: @merge_request.assignee.try(:avatar_url) - } + render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) end end else @@ -210,31 +207,41 @@ class Projects::MergeRequestsController < Projects::ApplicationController #This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @commit = @repository.commit(params[:ref]) if params[:ref].present? + render layout: false end def branch_to @target_project = selected_target_project @commit = @target_project.commit(params[:ref]) if params[:ref].present? + render layout: false end def update_branches @target_project = selected_target_project @target_branches = @target_project.repository.branch_names - respond_to do |format| - format.js - end + render layout: false end def ci_status - ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) + ci_commit = @merge_request.ci_commit + if ci_commit + status = ci_commit.status + coverage = ci_commit.try(:coverage) + else + ci_service = @merge_request.source_project.ci_service + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + if ci_service.respond_to?(:commit_coverage) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + end end + status = "preparing" if status.nil? + response = { + title: merge_request.title, + sha: merge_request.last_commit_short_sha, status: status, coverage: coverage } diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b2e974eff1..f7b6d137bd 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -19,13 +19,12 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - respond_to do |format| format.html do @milestones = @milestones.page(params[:page]) end format.json do - render json: @milestones + render json: @milestones.to_json(methods: :name) end end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd56804..707a0d0e5c 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController def destroy if note.editable? - note.destroy - note.reset_events_cache + Notes::DeleteService.new(project, current_user).execute(note) end respond_to do |format| diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f..e457db2f0b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -94,9 +94,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def apply_import - giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver, current_user) - notice = status ? "Successfully imported" : "Import failed" + source_project = Project.find(params[:source_project_id]) + + if can?(current_user, :read_project_member, source_project) + status = @project.team.import(source_project, current_user) + notice = status ? "Successfully imported" : "Import failed" + else + return render_404 + end redirect_to(namespace_project_project_members_path(project.namespace, project), notice: notice) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 00df1c9c96..d79f16e6a5 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -24,6 +24,8 @@ class Projects::RefsController < Projects::ApplicationController namespace_project_find_file_path(@project.namespace, @project, @id) when "graphs_commits" commits_namespace_project_graph_path(@project.namespace, @project, @id) + when "badges" + namespace_project_badges_path(@project.namespace, @project, ref: @id) else namespace_project_commits_path(@project.namespace, @project, @id) end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b578b419a4..6d2901a24a 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_action :authorize_read_project_snippet! + before_action :authorize_read_project_snippet!, except: [:new, :create, :index] # Allow write(create) snippet before_action :authorize_create_project_snippet!, only: [:new, :create] @@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end + def authorize_read_project_snippet! + return render_404 unless can?(current_user, :read_project_snippet, @snippet) + end + def authorize_update_project_snippet! return render_404 unless can?(current_user, :update_project_snippet, @snippet) end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 02ceb8f433..9f3a4a6972 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -88,6 +88,20 @@ class Projects::WikisController < Projects::ApplicationController ) end + def markdown_preview + text = params[:text] + + ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) + ext.analyze(text) + + render json: { + body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), + references: { + users: ext.users.map(&:username) + } + } + end + def git_access end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 928817ba81..3cc37e5985 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController def update status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + # Refresh the repo in case anything changed + @repository = project.repository + respond_to do |format| if status flash[:notice] = "Project '#{@project.name}' was successfully updated." @@ -71,7 +74,7 @@ class ProjectsController < Projects::ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.unlink_fork + if ::Projects::UnlinkForkService.new(@project, current_user).execute flash[:notice] = 'The fork relationship has been removed.' end end @@ -138,7 +141,7 @@ class ProjectsController < Projects::ApplicationController participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { - emojis: autocomplete_emojis, + emojis: AwardEmoji.urls, issues: autocomplete.issues, mergerequests: autocomplete.merge_requests, members: participants @@ -235,17 +238,6 @@ class ProjectsController < Projects::ApplicationController ) end - def autocomplete_emojis - Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do - Emoji.emojis.map do |name, emoji| - { - name: name, - path: view_context.image_url("#{emoji["unicode"]}.png") - } - end - end - end - def repo_exists? project.repository_exists? && !project.empty_repo? end diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index ad04c646e1..627be74a38 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController redirect_to activity_dashboard_path when 'starred_project_activity' redirect_to activity_dashboard_path(filter: 'starred') + when 'groups' + redirect_to dashboard_groups_path + when 'todos' + redirect_to dashboard_todos_path else return end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 65677a3dd3..c29f4609e9 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,7 +5,8 @@ class SessionsController < Devise::SessionsController skip_before_action :check_2fa_requirement, only: [:destroy] prepend_before_action :check_initial_setup, only: [:new] - prepend_before_action :authenticate_with_two_factor, only: [:create] + prepend_before_action :authenticate_with_two_factor, + if: :two_factor_enabled?, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] @@ -56,10 +57,10 @@ class SessionsController < Devise::SessionsController end def find_user - if user_params[:login] - User.by_login(user_params[:login]) - elsif user_params[:otp_attempt] && session[:otp_user_id] + if session[:otp_user_id] User.find(session[:otp_user_id]) + elsif user_params[:login] + User.by_login(user_params[:login]) end end @@ -83,11 +84,13 @@ class SessionsController < Devise::SessionsController end end + def two_factor_enabled? + find_user.try(:two_factor_enabled?) + end + def authenticate_with_two_factor user = self.resource = find_user - return unless user && user.two_factor_enabled? - if user_params[:otp_attempt].present? && session[:otp_user_id] if valid_otp_attempt?(user) # Remove any lingering user data from login diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 046286dd9e..f1df6832bf 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -243,7 +243,7 @@ class IssuableFinder end def filter_by_upcoming_milestone? - params[:milestone_title] == '#upcoming' + params[:milestone_title] == Milestone::Upcoming.name end def by_milestone(items) @@ -252,7 +252,7 @@ class IssuableFinder items = items.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.title }) + items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 23693629a4..60a0ff32c9 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,10 +3,6 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 820d69c230..9e59a295fc 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -27,9 +27,9 @@ module BlobHelper link_opts) if !on_top_of_branch?(project, ref) - 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' } + button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) - link_to "Edit", edit_path, class: 'btn' + link_to "Edit", edit_path, class: 'btn btn-file-option' elsif can?(current_user, :fork_project, project) continue_params = { to: edit_path, @@ -38,7 +38,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn', method: :post + link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index bde0799f3d..35ba543cef 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -28,7 +28,7 @@ module CommitsHelper def commit_to_html(commit, project, inline = true) template = inline ? "inline_commit" : "commit" - escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? + render "projects/commits/#{template}", commit: commit, project: project unless commit.nil? end # Breadcrumb links for a Project and, if applicable, a tree path @@ -117,7 +117,7 @@ module CommitsHelper end end link_to( - "Browse Files »", + "Browse Files", namespace_project_tree_path(project.namespace, project, commit), class: "pull-right" ) @@ -197,7 +197,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff.new_path)), - class: 'btn view-file js-view-file' + class: 'btn view-file js-view-file btn-file-option' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index ceff1fbb16..14697f774c 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -60,7 +60,7 @@ module DropdownsHelper title_output << content_tag(:span, title) title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do - icon('times') + icon('times', class: 'dropdown-menu-close-icon') end title_output.html_safe @@ -70,7 +70,8 @@ module DropdownsHelper def dropdown_filter(placeholder) content_tag :div, class: "dropdown-input" do filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder - filter_output << icon('search') + filter_output << icon('search', class: "dropdown-input-search") + filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") filter_output.html_safe end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index a67a6b208e..592bad8ba2 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -194,7 +194,7 @@ module EventsHelper end def event_to_atom(xml, event) - if event.proper?(current_user) + if event.visible_to_user?(current_user) xml.entry do event_link = event_feed_url(event) event_title = event_feed_title(event) @@ -214,4 +214,12 @@ module EventsHelper end end end + + def event_row_class(event) + if event.body? + "event-block" + else + "event-inline" + end + end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb new file mode 100644 index 0000000000..6a43be2cf3 --- /dev/null +++ b/app/helpers/form_helper.rb @@ -0,0 +1,18 @@ +module FormHelper + def form_errors(model) + return unless model.errors.any? + + pluralized = 'error'.pluralize(model.errors.count) + headline = "The form contains the following #{pluralized}:" + + content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do + content_tag(:h4, headline) << + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end + end + end +end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 2f760af02f..3a45205563 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -116,29 +116,6 @@ module GitlabMarkdownHelper end end - MARKDOWN_TIPS = [ - "End a line with two or more spaces for a line-break, or soft-return", - "Inline code can be denoted by `surrounding it with backticks`", - "Blocks of code can be denoted by three backticks ``` or four leading spaces", - "Emoji can be added by :emoji_name:, for example :thumbsup:", - "Notify other participants using @user_name", - "Notify a specific group using @group_name", - "Notify the entire team using @all", - "Reference an issue using a hash, for example issue #123", - "Reference a merge request using an exclamation point, for example MR !123", - "Italicize words or phrases using *asterisks* or _underscores_", - "Bold words or phrases using **double asterisks** or __double underscores__", - "Strikethrough words or phrases using ~~two tildes~~", - "Make a bulleted list using + pluses, - minuses, or * asterisks", - "Denote blockquotes using > at the beginning of a line", - "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___" - ].freeze - - # Returns a random markdown tip for use as a textarea placeholder - def random_markdown_tip - MARKDOWN_TIPS.sample - end - private # Return +text+, truncated to +max_chars+ characters, excluding any HTML diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 81df209439..b14b8218d0 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,16 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def issuable_json_path(issuable) + project = issuable.project + + if issuable.kind_of?(MergeRequest) + namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json) + else + namespace_project_issue_path(project.namespace, project, issuable.iid, :json) + end + end + def prev_issuable_for(issuable) base_issuable_scope(issuable).where('iid < ?', issuable.iid).first end @@ -37,6 +47,14 @@ module IssuablesHelper end end + def milestone_dropdown_label(milestone_title, default_label = "Milestone") + if milestone_title == Milestone::Upcoming.name + milestone_title = Milestone::Upcoming.title + end + + h(milestone_title.presence || default_label) + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 24b90fef4f..4cb8adceba 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -52,6 +52,7 @@ module IssuesHelper def milestone_options(object) milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a + milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) @@ -115,17 +116,32 @@ module IssuesHelper icon('eye-slash') if issue.confidential? end - def emoji_icon(name, unicode = nil, aliases = []) + def emoji_icon(name, unicode = nil, aliases = [], sprite: true) unicode ||= Emoji.emoji_filename(name) rescue "" - content_tag :div, "", - class: "icon emoji-icon emoji-#{unicode}", - title: name, - data: { - aliases: aliases.join(' '), - emoji: name, - unicode_name: unicode - } + data = { + aliases: aliases.join(" "), + emoji: name, + unicode_name: unicode + } + + if sprite + # Emoji icons for the emoji menu, these use a spritesheet. + content_tag :div, "", + class: "icon emoji-icon emoji-#{unicode}", + title: name, + data: data + else + # Emoji icons displayed separately, used for the awards already given + # to an issue or merge request. + content_tag :img, "", + class: "icon emoji", + title: name, + height: "20px", + width: "20px", + src: url_to_image("#{unicode}.png"), + data: data + end end def emoji_author_list(notes, current_user) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index faba418c4d..94c6b548ec 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -3,8 +3,16 @@ module NamespacesHelper groups = current_user.owned_groups + current_user.masters_groups users = [current_user.namespace] - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] + data_attr_group = { 'data-options-parent' => 'groups' } + data_attr_users = { 'data-options-parent' => 'users' } + + group_opts = [ + "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] } + ] + + users_opts = [ + "Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] } + ] options = [] options << group_opts diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 53c543c28c..95072b5373 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -5,8 +5,10 @@ module NotesHelper end def note_target_fields(note) - hidden_field_tag(:target_type, note.noteable.class.name.underscore) + - hidden_field_tag(:target_id, note.noteable.id) + if note.noteable + hidden_field_tag(:target_type, note.noteable.class.name.underscore) + + hidden_field_tag(:target_id, note.noteable.id) + end end def note_editable?(note) @@ -67,10 +69,7 @@ module NotesHelper line_type: line_type } - button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button', - data: data, title: 'Add a reply' do - link_text = icon('comment') - link_text << ' Reply' - end + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', + data: data, title: 'Add a reply' end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index c73cb3028e..c3832cf5d6 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -12,7 +12,9 @@ module PreferencesHelper projects: 'Your Projects (default)', stars: 'Starred Projects', project_activity: "Your Projects' Activity", - starred_project_activity: "Starred Projects' Activity" + starred_project_activity: "Starred Projects' Activity", + groups: "Your Groups", + todos: "Your Todos" }.with_indifferent_access.freeze # Returns an Array usable by a select field for more user-friendly option text diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 494dad0b41..8a97a74ad7 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,4 +1,5 @@ module SearchHelper + def search_autocomplete_opts(term) return unless current_user @@ -23,45 +24,44 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "SSH Keys", url: profile_keys_path }, + { category: "Settings", label: "Dashboard", url: root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api", "README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -72,7 +72,9 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - label: "group: #{search_result_sanitize(group.name)}", + category: "Groups", + id: group.id, + label: "#{search_result_sanitize(group.name)}", url: group_path(group) } end @@ -83,7 +85,10 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", + category: "Projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.name_with_namespace)}", url: namespace_project_path(p.namespace, p) } end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 8cbc9eefc7..826e5f96fa 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -110,6 +110,10 @@ class Notify < BaseMailer headers['Reply-To'] = address + fallback_reply_message_id = "".freeze + headers['References'] ||= '' + headers['References'] << ' ' << fallback_reply_message_id + @reply_by_email = true end diff --git a/app/models/ability.rb b/app/models/ability.rb index fa2345f6fa..c0bf6def7c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -27,6 +27,8 @@ class Ability case true when subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) + when subject.is_a?(ProjectSnippet) + anonymous_project_snippet_abilities(subject) when subject.is_a?(CommitStatus) anonymous_commit_status_abilities(subject) when subject.is_a?(Project) || subject.respond_to?(:project) @@ -100,6 +102,14 @@ class Ability end end + def anonymous_project_snippet_abilities(snippet) + if snippet.public? + [:read_project_snippet] + else + [] + end + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group @@ -338,24 +348,22 @@ class Ability end end - [:note, :project_snippet].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] + def note_abilities(user, note) + rules = [] - if subject.author == user - rules += [ - :"read_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end - - if subject.respond_to?(:project) && subject.project - rules += project_abilities(user, subject.project) - end - - rules + if note.author == user + rules += [ + :read_note, + :update_note, + :admin_note + ] end + + if note.respond_to?(:project) && note.project + rules += project_abilities(user, note.project) + end + + rules end def personal_snippet_abilities(user, snippet) @@ -376,6 +384,24 @@ class Ability rules end + def project_snippet_abilities(user, snippet) + rules = [] + + if snippet.author == user || user.admin? + rules += [ + :read_project_snippet, + :update_project_snippet, + :admin_project_snippet + ] + end + + if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) + rules << :read_project_snippet + end + + rules + end + def group_member_abilities(user, subject) rules = [] target_user = subject.user diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c4879598c4..052cd87473 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null @@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/app/models/commit.rb b/app/models/commit.rb index ce0b85d50c..d09876a07d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -74,14 +74,14 @@ class Commit # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?\h{7,40})/) end def to_reference(from_project = nil) @@ -230,7 +230,7 @@ class Commit end def revert_message - %Q{Revert "#{title}"\n\n#{revert_description}} + %Q{Revert "#{title.strip}"\n\n#{revert_description}} end def reverts_commit?(commit) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 289dbc5728..51673897d9 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -43,14 +43,14 @@ class CommitRange # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?#{STRICT_PATTERN}) }x end def self.link_reference_pattern - super("compare", /(?#{PATTERN})/) + @link_reference_pattern ||= super("compare", /(?#{PATTERN})/) end # Initialize a CommitRange diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index cf5b2c7167..afa2ca039a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :todos, as: :target, dependent: :destroy validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -41,7 +42,7 @@ module Issuable scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } - scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) } + scope :non_archived, -> { join_project.where(projects: { archived: false }) } delegate :name, :email, diff --git a/app/models/event.rb b/app/models/event.rb index a5cfeaf388..12183524b7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -73,15 +73,15 @@ class Event < ActiveRecord::Base end end - def proper?(user = nil) + def visible_to_user?(user = nil) if push? true elsif membership_changed? true elsif created_project? true - elsif issue? - Ability.abilities.allowed?(user, :read_issue, issue) + elsif issue? || issue_note? + Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) else ((merge_request? || note?) && target) || milestone? end @@ -298,6 +298,10 @@ class Event < ActiveRecord::Base target.noteable_type == "Commit" end + def issue_note? + note? && target && target.noteable_type == "Issue" + end + def note_project_snippet? target.noteable_type == "Snippet" end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 2ca79df0a2..b8585d4e57 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} + @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 97bd79af08..da7c265a37 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -14,6 +14,7 @@ class GlobalMilestone def initialize(title, milestones) @title = title + @name = title @milestones = milestones end diff --git a/app/models/issue.rb b/app/models/issue.rb index ed960cb39f..e064b0f8b9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("issues", /(?\d+)/) + @link_reference_pattern ||= super("issues", /(?\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index 500d5a3552..55c01cae76 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -56,7 +56,7 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ef48207f95..bf185cb5dd 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("merge_requests", /(?\d+)/) + @link_reference_pattern ||= super("merge_requests", /(?\d+)/) end # Returns all the merge requests from an ActiveRecord:Relation. @@ -279,7 +280,7 @@ class MergeRequest < ActiveRecord::Base WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze def work_in_progress? - title =~ WIP_REGEX + !!(title =~ WIP_REGEX) end def wipless_title @@ -331,15 +332,15 @@ class MergeRequest < ActiveRecord::Base # Returns the raw diff for this merge request # # see "git diff" - def to_diff(current_user) - target_project.repository.diff_text(target_branch, source_sha) + def to_diff + target_project.repository.diff_text(diff_base_commit.sha, source_sha) end # Returns the commit as a series of email patches. # # see "git format-patch" - def to_patch(current_user) - target_project.repository.format_patch(target_branch, source_sha) + def to_patch + target_project.repository.format_patch(diff_base_commit.sha, source_sha) end def hook_attrs diff --git a/app/models/milestone.rb b/app/models/milestone.rb index de7183bf6b..986184dd30 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,17 +79,17 @@ class Milestone < ActiveRecord::Base end def self.link_reference_pattern - super("milestones", /(?\d+)/) + @link_reference_pattern ||= super("milestones", /(?\d+)/) end def self.upcoming - self.where('due_date > ?', Time.now).order(due_date: :asc).first + self.where('due_date > ?', Time.now).reorder(due_date: :asc).first end def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) "[#{escaped_title}](#{url})" diff --git a/app/models/note.rb b/app/models/note.rb index b0c33f2eec..87ced65c65 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -311,7 +311,7 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end - def for_project_snippet? + def for_snippet? noteable_type == "Snippet" end diff --git a/app/models/project.rb b/app/models/project.rb index 9c8246e8ac..3e1f04b415 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -206,6 +206,8 @@ class Project < ActiveRecord::Base mount_uploader :avatar, AvatarUploader # Scopes + default_scope { where(pending_delete: false) } + scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } @@ -304,7 +306,7 @@ class Project < ActiveRecord::Base end def find_with_namespace(id) - namespace_path, project_path = id.split('/') + namespace_path, project_path = id.split('/', 2) return nil if !namespace_path || !project_path @@ -469,7 +471,7 @@ class Project < ActiveRecord::Base end def web_url - Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -590,7 +592,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -929,16 +931,6 @@ class Project < ActiveRecord::Base self.builds_enabled = true end - def unlink_fork - if forked? - forked_from_project.lfs_objects.find_each do |lfs_object| - lfs_object.projects << self - end - - forked_project_link.destroy - end - end - def any_runners?(&block) if runners.active.any?(&block) return true diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f6313255cb..f9f0483876 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -50,12 +50,15 @@ class BuildsEmailService < Service def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) + return unless should_build_be_notified?(push_data) - if should_build_be_notified?(push_data) + recipients = all_recipients(push_data) + + if recipients.any? BuildEmailWorker.perform_async( push_data[:build_id], - all_recipients(push_data), - push_data, + recipients, + push_data ) end end @@ -84,7 +87,7 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',') + all_recipients = recipients.split(',').compact.reject(&:blank?) if add_pusher? && data[:user][:email] all_recipients << "#{data[:user][:email]}" diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05436cd0f7..eaa5654b9c 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,7 +20,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aba37921c0..1ed42c4f3e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,7 +21,7 @@ class JiraService < IssueTrackerService include HTTParty - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers DEFAULT_API_VERSION = 2 diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 5af24a8060..438ff33fdf 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -22,7 +22,7 @@ class SlackService @issue_url = obj_attr[:url] @action = obj_attr[:action] @state = obj_attr[:state] - @description = obj_attr[:description] + @description = obj_attr[:description] || '' end def attachments diff --git a/app/models/repository.rb b/app/models/repository.rb index 13154eb420..462b48118e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -72,7 +72,7 @@ class Repository return @has_visible_content unless @has_visible_content.nil? @has_visible_content = cache.fetch(:has_visible_content?) do - raw_repository.branch_count > 0 + branch_count > 0 end end @@ -173,7 +173,7 @@ class Repository end def branch_names - cache.fetch(:branch_names) { raw_repository.branch_names } + cache.fetch(:branch_names) { branches.map(&:name) } end def tag_names @@ -191,7 +191,7 @@ class Repository end def branch_count - @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + @branch_count ||= cache.fetch(:branch_count) { branches.size } end def tag_count @@ -239,7 +239,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) - @branches = nil + @local_branches = nil end def expire_cache(branch_name = nil, revision = nil) @@ -331,10 +331,14 @@ class Repository # Runs code after a repository has been created. def after_create expire_exists_cache + expire_root_ref_cache + expire_emptiness_caches end # Runs code just before a repository is deleted. def before_delete + expire_exists_cache + expire_cache if exists? expire_root_ref_cache @@ -362,6 +366,11 @@ class Repository expire_tag_count_cache end + def before_import + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been forked/imported. def after_import expire_emptiness_caches @@ -467,6 +476,18 @@ class Repository end end + def gitlab_ci_yml + return nil if !exists? || empty? + + @gitlab_ci_yml ||= tree(:head).blobs.find do |file| + file.name == '.gitlab-ci.yml' + end + rescue Rugged::ReferenceError + # For unknow reason spinach scenario "Scenario: I change project path" + # lead to "Reference 'HEAD' not found" exception from Repository#empty? + nil + end + def head_commit @head_commit ||= commit(self.root_ref) end @@ -600,10 +621,14 @@ class Repository refs_contains_sha('tag', sha) end - def branches - @branches ||= raw_repository.branches + def local_branches + @local_branches ||= rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(branch.name, branch.target) + end end + alias_method :branches, :local_branches + def tags @tags ||= raw_repository.tags end @@ -806,7 +831,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end @@ -871,12 +896,14 @@ class Repository end def main_language - unless empty? - Linguist::Repository.new(rugged, rugged.head.target_id).language - end + return if empty? || rugged.head_unborn? + + Linguist::Repository.new(rugged, rugged.head.target_id).language end def avatar + return nil unless exists? + @avatar ||= cache.fetch(:avatar) do AVATAR_FILES.find do |file| blob_at_branch('master', file) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b9e835a448..b96e393728 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("snippets", /(?\d+)/) + @link_reference_pattern ||= super("snippets", /(?\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/user.rb b/app/models/user.rb index 9c315cfe96..2b0bee2099 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -184,7 +184,7 @@ class User < ActiveRecord::Base # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity] + enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos] # User's Project preference # Note: When adding an option, it MUST go on the end of the array. @@ -408,6 +408,8 @@ class User < ActiveRecord::Base end def owns_notification_email + return if self.temp_oauth_email? + self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c007d648dd..dc74c02760 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -43,23 +43,27 @@ class GitPushService < BaseService @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages end - # Checks if the main language has changed in the project and if so - # it updates it accordingly - update_main_language # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests + # Checks if the main language has changed in the project and if so + # it updates it accordingly + update_main_language + perform_housekeeping end def update_main_language + # Performance can be bad so for now only check main_language once + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937 + return if @project.main_language.present? + + return unless is_default_branch? + return unless push_to_new_branch? || push_to_existing_branch? + current_language = @project.repository.main_language - - unless current_language == @project.main_language - return @project.update_attributes(main_language: current_language) - end - + @project.update_attributes(main_language: current_language) true end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 468f8acdf6..82e7090f1e 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -43,7 +43,7 @@ module Issues def create_new_issue new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, project: @new_project, author: @old_issue.author, - description: unfold_references(@old_issue.description) } + description: rewrite_content(@old_issue.description) } new_params = @old_issue.serializable_hash.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute @@ -53,13 +53,26 @@ module Issues @old_issue.notes.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, - note: unfold_references(new_note.note), - created_at: note.created_at } + note: rewrite_content(new_note.note), + created_at: note.created_at, + updated_at: note.updated_at } new_note.update(new_params) end end + def rewrite_content(content) + return unless content + + rewriters = [Gitlab::Gfm::ReferenceRewriter, + Gitlab::Gfm::UploadsRewriter] + + rewriters.inject(content) do |text, klass| + rewriter = klass.new(text, @old_project, @current_user) + rewriter.rewrite(@new_project) + end + end + def close_issue close_service = CloseService.new(@old_project, @current_user) close_service.execute(@old_issue, notifications: false, system_note: false) @@ -77,20 +90,12 @@ module Issues direction: :to) end - def unfold_references(content) - return unless content - - rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, - @current_user) - rewriter.rewrite(@new_project) + def mark_as_moved + @old_issue.update(moved_to: @new_issue) end def notify_participants notification_service.issue_moved(@old_issue, @new_issue, @current_user) end - - def mark_as_moved - @old_issue.update(moved_to: @new_issue) - end end end diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index b8e08c9f1e..3b90399af6 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -3,7 +3,7 @@ module Milestones def execute milestone = project.milestones.new(params) - if milestone.save + if milestone.save! event_service.open_milestone(milestone, current_user) end diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb new file mode 100644 index 0000000000..7f1b30ec84 --- /dev/null +++ b/app/services/notes/delete_service.rb @@ -0,0 +1,8 @@ +module Notes + class DeleteService < BaseService + def execute(note) + note.destroy + note.reset_events_cache + end + end +end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 2015897dd1..ef15ef6a47 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -46,6 +46,8 @@ module Projects def import_data return unless has_importer? + project.repository.before_import + unless importer.execute raise Error, 'The remote data could not be imported.' end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb new file mode 100644 index 0000000000..315c3e1629 --- /dev/null +++ b/app/services/projects/unlink_fork_service.rb @@ -0,0 +1,19 @@ +module Projects + class UnlinkForkService < BaseService + def execute + return unless @project.forked? + + @project.forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project + end + + merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + + merge_requests.each do |mr| + MergeRequests::CloseService.new(@project, @current_user).execute(mr) + end + + @project.forked_project_link.destroy + end + end +end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index ea2b26ccb5..f0615ec742 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -95,17 +95,19 @@ class SystemHooksService end def project_member_data(model) + project = model.project || Project.unscoped.find(model.source_id) + { - project_name: model.project.name, - project_path: model.project.path, - project_path_with_namespace: model.project.path_with_namespace, - project_id: model.project.id, - user_username: model.user.username, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + project_name: project.name, + project_path: project.path, + project_path_with_namespace: project.path_with_namespace, + project_id: project.id, + user_username: model.user.username, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e022a046c4..658b086496 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -224,7 +224,7 @@ class SystemNoteService # # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f2662922e9..42c5bca90f 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -123,7 +123,7 @@ class TodoService def handle_note(note, author) # Skip system notes, and notes on project snippet - return if note.system? || note.for_project_snippet? + return if note.system? || note.for_snippet? project = note.project target = note.noteable @@ -170,14 +170,30 @@ class TodoService end def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users.select do |user| - user.can?(:read_project, project) - end - + mentioned_users = target.mentioned_users + mentioned_users = reject_users_without_access(mentioned_users, project, target) mentioned_users.delete(author) mentioned_users.uniq end + def reject_users_without_access(users, project, target) + if target.is_a?(Note) && target.for_issue? + target = target.noteable + end + + if target.is_a?(Issue) + select_users(users, :read_issue, target) + else + select_users(users, :read_project, project) + end + end + + def select_users(users, ability, subject) + users.select do |user| + user.can?(ability.to_sym, subject) + end + end + def pending_todos(user, criteria = {}) valid_keys = [:project_id, :target_id, :target_type, :commit_id] user.todos.pending.where(criteria.slice(*valid_keys)) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 86d24469e0..1af9e9b0ed 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,14 +1,15 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?[0-9a-f]{32})/(?.*?)\)} storage :file attr_accessor :project, :secret - def initialize(project, secret = self.class.generate_secret) + def initialize(project, secret = nil) @project = project - @secret = secret + @secret = secret || self.class.generate_secret end def base_dir @@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def self.generate_secret - SecureRandom.hex - end - def secure_url File.join("/uploads", @secret, file.filename) end + def to_markdown + to_h[:markdown] + end + def to_h filename = image? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") @@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base markdown: markdown } end + + def self.generate_secret + SecureRandom.hex + end end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 3bc1b24b5e..06be1a5331 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -3,11 +3,9 @@ %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr = form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f| + = form_errors(@abuse_report) + = f.hidden_field :user_id - - if @abuse_report.errors.any? - .alert.alert-danger - - @abuse_report.errors.full_messages.each do |msg| - %p= msg .form-group = f.label :user_id, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 6f325914d1..d88f3ad314 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -1,8 +1,5 @@ = form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| - - if @appearance.errors.any? - .alert.alert-danger - - @appearance.errors.full_messages.each do |msg| - %p= msg + = form_errors(@appearance) %fieldset.sign-in %legend diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0350995d03..a8cca1a81c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -1,9 +1,5 @@ = form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @application_setting.errors.any? - #error_explanation - .alert.alert-danger - - @application_setting.errors.full_messages.each do |msg| - %p= msg + = form_errors(@application_setting) %fieldset %legend Visibility and Access Controls @@ -76,13 +72,6 @@ = f.label :gravatar_enabled do = f.check_box :gravatar_enabled Gravatar enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :twitter_sharing_enabled do - = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - Twitter enabled - %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter .form-group = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index e18f7b499d..4aacbb8cd7 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,9 +1,6 @@ = form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - - application.errors.full_messages.each do |msg| - %p= msg + = form_errors(application) + = content_tag :div, class: 'form-group' do = f.label :name, class: 'col-sm-2 control-label' .col-sm-10 diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index b748460a9f..6b157abf84 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -4,10 +4,8 @@ = render_broadcast_message(@broadcast_message.message.presence || "Your message here") = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| - -if @broadcast_message.errors.any? - .alert.alert-danger - - @broadcast_message.errors.full_messages.each do |msg| - %p= msg + = form_errors(@broadcast_message) + .form-group = f.label :message, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 588ad76742..3571eefd57 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -15,7 +15,7 @@ %td - if project - = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project) %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3274ba5377..6dd2fef395 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,4 +1,4 @@ -.admin-dashboard +.admin-dashboard.prepend-top-default .row .col-md-4 %h4 Statistics diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 41c4389997..149593e7f4 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,5 +1,5 @@ - page_title "Deploy Keys" -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading Public deploy keys (#{@deploy_keys.count}) .controls diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 5b46b3222a..15aa059c93 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -4,11 +4,7 @@ %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| - -if @deploy_key.errors.any? - .alert.alert-danger - %ul - - @deploy_key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@deploy_key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 7f2b1cd235..0cc405401c 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,8 +1,5 @@ = form_for [:admin, @group], html: { class: "form-horizontal" } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f .form-group.group-description-holder diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml new file mode 100644 index 0000000000..9025aaac09 --- /dev/null +++ b/app/views/admin/groups/_group.html.haml @@ -0,0 +1,28 @@ +- css_class = '' unless local_assigns[:css_class] +- css_class += ' no-description' if group.description.blank? + +%li.group-row{ class: css_class } + .controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' + + .stats + %span + = icon('bookmark') + = number_with_delimiter(group.projects.count) + + %span + = icon('users') + = number_with_delimiter(group.users.count) + + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + = visibility_level_icon(group.visibility_level, fw: false) + + = image_tag group_icon(group), class: 'avatar s40 hidden-xs' + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 6bdc885a31..775072a744 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,20 +1,19 @@ - page_title "Groups" %h3.page-title Groups (#{number_with_delimiter(@groups.total_count)}) - = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %p.light Group allows you to keep projects organized. Use groups for uniting related projects. -%hr -= form_tag admin_groups_path, method: :get, class: 'form-inline' do - = hidden_field_tag :sort, @sort - .form-group - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" +.top-area + .nav-search + = form_tag admin_groups_path, method: :get, class: 'form-inline' do + = hidden_field_tag :sort, @sort + = text_field_tag :name, params[:name], class: "form-control" + = button_tag "Search", class: "btn submit btn-primary" - .pull-right + .nav-controls .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %span.light @@ -33,34 +32,10 @@ = sort_title_recently_updated = link_to admin_groups_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" -%hr - -%ul.bordered-list +%ul.content-list - @groups.each do |group| - %li - .clearfix - .pull-right.prepend-top-10 - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove" - - %h4 - = link_to [:admin, group] do - %span{ class: visibility_level_color(group.visibility_level) } - = visibility_level_icon(group.visibility_level) - - %i.fa.fa-folder - = group.name - - → - %span.monospace - %strong #{group.path}/ - .clearfix - %p - = truncate group.description, length: 150 - .clearfix - %p.light - #{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')} - + = render 'group', group: group = paginate @groups, theme: "gitlab" diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 53b3cd04c6..ad952052f2 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -10,10 +10,8 @@ = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL:", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 3a78855822..112a201faf 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -1,9 +1,5 @@ = form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| - - if @identity.errors.any? - #error_explanation - .alert.alert-danger - - @identity.errors.full_messages.each do |msg| - %p= msg + = form_errors(@identity) .form-group = f.label :provider, class: 'control-label' diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 8c6b389bf1..448aa95354 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 3c57e3dc17..05d6b9ed23 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -1,8 +1,10 @@ - page_title "Labels" -= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do - New label -%h3.page-title - Labels + +%div + = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do + New label + %h3.page-title + Labels %hr .labels @@ -13,4 +15,4 @@ - else .light-well .nothing-here-block There are no labels yet - + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index c407972cd0..2dad64b8d0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,4 +1,4 @@ -%p.lead +%p.lead.prepend-top-default %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index d2527ede99..b05fdbd555 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,10 +1,6 @@ .user_new = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f| - -if @user.errors.any? - #error_explanation - .alert.alert-danger - - @user.errors.full_messages.each do |msg| - %p= msg + = form_errors(@user) %fieldset %legend Account diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml deleted file mode 100644 index 9c2290bc4a..0000000000 --- a/app/views/ci/projects/index.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.wiki - %h1 - GitLab CI is now integrated in GitLab UI - %h2 For existing projects - - %p - Check the following pages to find the CI status you're looking for: - - %ul - %li Projects page - shows CI status for each project. - %li Project commits page - show CI status for each commit. - - - - %h2 For new projects - - %p - If you want to enable CI for a new project it is easy as adding - = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html" - file to your repository diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index e3a4d64df0..aa0aff86d4 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,4 +1,4 @@ -%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } +%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' @@ -10,7 +10,10 @@ (removed) %span.todo-label = todo_action_name(todo) - = todo_target_link(todo) + - if todo.target + = todo_target_link(todo) + - else + (removed) · #{time_ago_with_tooltip(todo.created_at)} diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 906b067615..5c98265727 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -1,9 +1,5 @@ = form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %ul - - application.errors.full_messages.each do |msg| - %li= msg + = form_errors(application) .form-group = f.label :name, class: 'label-light' diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index ea0b66c932..55f4a6f287 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -77,7 +77,7 @@ %em Authorization was granted by entering your username and password in the application. %td= token.created_at %td= token.scopes - %td= render 'delete_form', token: token + %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else .profile-settings-message.text-center You don't have any authorized applications diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 2d9d9dd634..4d20dd5830 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ -- if event.proper?(current_user) - .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} +- if event.visible_to_user?(current_user) + .event-item{ class: event_row_class(event) } .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 8cf36c711b..5a2a469ba6 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -7,21 +7,3 @@ = link_to_project event.project - else = event.project_name - -- if !event.project.private? && twitter_sharing_enabled? - .event-body{"data-user-is" => event.author_id} - .event-note - .md - %p - Congratulations! Why not share your accomplishment with the world? - - %a.twitter-share-button{ | - href: "https://twitter.com/share", | - "data-url" => event.project.web_url, | - "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | - "data-size" => "medium", | - "data-related" => "gitlab", | - "data-hashtags" => "gitlab", | - "data-count" => "none"} - Tweet - %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index ea5a035839..a698cbbe9d 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -5,9 +5,7 @@ Group settings .panel-body = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first + = form_errors(@group) = render 'shared/group_form', f: f .form-group diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index a8e1ed77da..4290e0bf72 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -10,6 +10,14 @@ = form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| .row + - if @milestone.errors.any? + #error_explanation + .alert.alert-danger + %ul + - @milestone.errors.full_messages.each do |msg| + %li + = msg + .col-md-6 .form-group = f.label :title, "Title", class: "control-label" diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 30ab8aeba1..2b8bc269e6 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -6,10 +6,7 @@ %hr = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true .form-group.group-description-holder diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 820743dc8d..3d16ecb097 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -17,7 +17,7 @@ .cover-title %h1 = @group.name - %span.visibility-icon.has_tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } = visibility_level_icon(@group.visibility_level, fw: false) .cover-desc.username diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 54af2c3063..6b208c3d0b 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,10 +1,33 @@ -.search - = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" - = hidden_field_tag :group_id, @group.try(:id) - - if @project && @project.persisted? - = hidden_field_tag :project_id, @project.id +- if controller.controller_path =~ /^groups/ && @group.persisted? + - label = 'This group' +- if controller.controller_path =~ /^projects/ && @project.persisted? + - label = 'This project' +.search.search-form{class: "#{'has-location-badge' if label.present?}"} + = form_tag search_path, method: :get, class: 'navbar-form' do |f| + .search-input-container + .search-location-badge + - if label.present? + %span.location-badge + %i.location-text + = label + .search-input-wrap + .dropdown{ data: {url: search_autocomplete_path } } + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } + .dropdown-menu.dropdown-select + = dropdown_content do + %ul + %li + %a.is-focused.dropdown-menu-empty-link + Loading... + = dropdown_loading + %i.search-icon + %i.clear-icon.js-clear-input + + = hidden_field_tag :group_id, @group.try(:id) + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' + + - if @project && @project.persisted? - if current_controller?(:issues) = hidden_field_tag :scope, 'issues' - elsif current_controller?(:merge_requests) @@ -21,10 +44,3 @@ = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } - -:javascript - $('.search-input').on('keyup', function(e) { - if (e.keyCode == 27) { - $('.search-input').blur(); - } - }); diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4a0069f18f..5cef652da1 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -9,7 +9,7 @@ = icon('bell fw') %span Todos - %span.count= number_with_delimiter(todos_pending_count) + %span.count.todos-pending-count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do = icon('dashboard fw') diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index dc3050f02e..d429a92846 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -51,8 +51,13 @@ = icon('code fw') %span Variables - = nav_link path: 'triggers#index' do + = nav_link(controller: :triggers) do = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = icon('retweet fw') %span Triggers + = nav_link(controller: :badges) do + = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + = icon('star-half-empty fw') + %span + Badges diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index ab527e8e43..a7ef31acd3 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -5,10 +5,14 @@ - content_for :scripts_body_top do - project = @target_project || @project + - if @project_wiki + - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project) + - else + - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - if current_user :javascript window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}"; + window.markdown_preview_path = "#{markdown_preview_path}"; - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 4d78215ed3..b3ed59a1a4 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f| - - if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :key, class: 'label-light' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index de80abd7f4..6609295a2a 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -2,11 +2,7 @@ - header_title page_title, profile_notifications_path = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - -if @user.errors.any? - %div.alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) = hidden_field_tag :notification_type, 'global' .row @@ -56,19 +52,20 @@ .prepend-top-default = f.submit 'Update settings', class: "btn btn-create" %hr - %h5 - Groups (#{@group_members.count}) - %div - %ul.bordered-list - - @group_members.each do |group_member| - - notification = Notification.new(group_member) - = render 'settings', type: 'group', membership: group_member, notification: notification - %h5 - Projects (#{@project_members.count}) - %p.account-well - To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. - .append-bottom-default - %ul.bordered-list - - @project_members.each do |project_member| - - notification = Notification.new(project_member) - = render 'settings', type: 'project', membership: project_member, notification: notification +.col-lg-9.col-lg-push-3 + %h5 + Groups (#{@group_members.count}) + %div + %ul.bordered-list + - @group_members.each do |group_member| + - notification = Notification.new(group_member) + = render 'settings', type: 'group', membership: group_member, notification: notification + %h5 + Projects (#{@project_members.count}) + %p.account-well + To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. + .append-bottom-default + %ul.bordered-list + - @project_members.each do |project_member| + - notification = Notification.new(project_member) + = render 'settings', type: 'project', membership: project_member, notification: notification diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index afd4f996b6..5ac8a8b9d0 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -13,23 +13,21 @@ - unless @user.password_automatically_set? or recover your current one = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f| - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + - unless @user.password_automatically_set? .form-group = f.label :current_password, class: 'label-light' = f.password_field :current_password, required: true, class: 'form-control' %p.help-block You must provide your current password in order to change it. - .form-group - = f.label :password, 'New password', class: 'label-light' - = f.password_field :password, required: true, class: 'form-control' - .form-group - = f.label :password_confirmation, class: 'label-light' - = f.password_field :password_confirmation, required: true, class: 'form-control' - .prepend-top-default.append-bottom-default - = f.submit 'Save password', class: "btn btn-create append-right-10" + .form-group + = f.label :password, 'New password', class: 'label-light' + = f.password_field :password, required: true, class: 'form-control' + .form-group + = f.label :password_confirmation, class: 'label-light' + = f.password_field :password_confirmation, required: true, class: 'form-control' + .prepend-top-default.append-bottom-default + = f.submit 'Save password', class: "btn btn-create append-right-10" + - unless @user.password_automatically_set? = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link" diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index d165f758c8..2eb9fac57c 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -7,11 +7,8 @@ Please set a new password before proceeding. %br After a successful password update you will be redirected to login screen. - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + + = form_errors(@user) - unless @user.password_automatically_set? .form-group diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index cd582ba706..f59d27f7ed 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,9 +1,6 @@ = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| - -if @user.errors.any? - %div.alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + .row .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 @@ -26,7 +23,7 @@ %a.btn.js-choose-user-avatar-button Browse file... %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-user-avatar-input hidden" + = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*" .help-block The maximum file size allowed is 200KB. - if @user.avatar? @@ -94,3 +91,25 @@ .prepend-top-default.append-bottom-default = f.submit 'Update profile settings', class: "btn btn-success" = link_to "Cancel", user_path(current_user), class: "btn btn-cancel" + +.modal.modal-profile-crop + .modal-dialog + .modal-content + .modal-header + %button.close{:type => "button", :'data-dismiss' => "modal"} + %span + × + %h4.modal-title + Position and size your new avatar + .modal-body + .profile-crop-image-container + %img.modal-profile-crop-image + .crop-controls + .btn-group + %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } + %span.fa.fa-search-plus + %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } + %span.fa.fa-search-minus + .modal-footer + %button.btn.btn-primary.js-upload-user-avatar{:type => "button"} + Set new profile picture diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 5d342ef58e..69fc81cb45 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -7,8 +7,6 @@ %p Increase your account's security by enabling two-factor authentication (2FA). .col-lg-9 - %p - Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} %p Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 95ab9ecf3e..9ae6964aaa 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -1,6 +1,14 @@ %fieldset.builds-feature %legend Builds: + + - unless @repository.gitlab_ci_yml + .form-group + .col-sm-offset-2.col-sm-10 + %p Builds need to be configured before you can begin using Continuous Integration. + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + %hr + .form-group .col-sm-offset-2.col-sm-10 %p Get recent application code using the following command: diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml index 7c8bb33ed7..2dba22d3be 100644 --- a/app/views/projects/_errors.html.haml +++ b/app/views/projects/_errors.html.haml @@ -1,4 +1 @@ -- if @project.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - = @project.errors.full_messages.first += form_errors(@project) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 514cbfa339..9b5de17dd3 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ .cover-title.project-home-desc %h1 = @project.name - %span.visibility-icon.has_tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} = visibility_level_icon(@project.visibility_level, fw: false) - if @project.description.present? diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 1fb37ef662..7a78d61a61 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,18 +1,19 @@ .md-area - .md-header.clearfix + .md-header %ul.nav-links %li.active - %a.js-md-write-button(href="#md-write-holder" tabindex="-1") + %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write %li - %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") + %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + %li.pull-right + %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + Go full screen - %div - .md-write-holder - = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} + .md-write-holder + = yield + .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users %div.referenced-users.hide diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e701253d7d..bddff5cdcb 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,12 +1,8 @@ -.zennable - .zen-backdrop - - classes << ' js-gfm-input js-autosize markdown-area' - - if defined?(f) && f - = f.text_area attr, class: classes - - else - = text_area_tag attr, nil, class: classes - %a.js-zen-enter(tabindex="-1" href="#") - = icon('expand') - Edit in fullscreen - %a.js-zen-leave(tabindex="-1" href="#") - = icon('compress') +.zen-backdrop + - classes << ' js-gfm-input js-autosize markdown-area' + - if defined?(f) && f + = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + - else + = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + = icon('compress') diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml new file mode 100644 index 0000000000..c22384ddf4 --- /dev/null +++ b/app/views/projects/badges/index.html.haml @@ -0,0 +1,24 @@ +- page_title 'Badges' +- badges_path = namespace_project_badges_path(@project.namespace, @project) +- header_title project_title(@project, 'Badges', badges_path) + +.prepend-top-10 + .panel.panel-default + .panel-heading + %b Builds badge · + = @build_badge.to_html + .pull-right + = render 'shared/ref_switcher', destination: 'badges' + .panel-body + .row + .col-md-2.text-center + Markdown + .col-md-10.code.js-syntax-highlight + = highlight('.md', @build_badge.to_markdown) + .row + %hr + .row + .col-md-2.text-center + HTML + .col-md-10.code.js-syntax-highlight + = highlight('.html', @build_badge.to_html) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7afea5a504..88266e2123 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -16,7 +16,7 @@ - else Name %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li = link_to namespace_project_branches_path(sort: nil) do Name diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 811d304ea7..aa85f495e3 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -27,6 +27,9 @@ = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + = link_to ci_lint_path, class: 'btn btn-default' do = icon('wrench') %span CI Lint diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index e7c85edff9..1e4c46fca2 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -3,25 +3,32 @@ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - if can?(current_user, :create_issue, @project) + - can_create_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - can_create_snippet = can?(current_user, :create_snippet, @project) + + - if can_create_issue %li = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - - if can?(current_user, :create_snippet, @project) + + - if can_create_snippet %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet - - if can?(current_user, :push_code, @project) + - if can_create_issue || merge_project || can_create_snippet %li.divider + + - if can?(current_user, :push_code, @project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') @@ -35,13 +42,11 @@ = icon('tags fw') New tag - elsif current_user && current_user.already_forked?(@project) - %li.divider %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') New file - elsif can?(current_user, :fork_project, @project) - %li.divider %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 88cbb7c03c..5fb5fe5af2 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,7 @@ = 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 + = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do %span.arrow %span.count = @project.forks_count diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index d22d1da840..2cf9115e4d 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -39,7 +39,7 @@ %td = build.name - .pull-right + .label-container - if build.tags.any? - build.tags.each do |tag| %span.label.label-primary diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7f2903589a..7da8923124 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -19,24 +19,17 @@ .pull-right - if ci_commit = render_ci_status(ci_commit) -   = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - .notes_count - - if note_count > 0 - %span.light - %i.fa.fa-comments - = note_count - - if commit.description? .commit-row-description.js-toggle-content %pre = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) .commit-row-info + by = commit_author_link(commit, avatar: true, size: 24) - authored .committed_ago #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}   = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index bac9e244d3..46e4de4004 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -5,10 +5,10 @@ .panel-heading Commits (#{@commits.count}) - if hidden > 0 - %ul.well-list + %ul.content-list - commits.each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.well-list= render commits, project: @project + %ul.content-list= render commits, project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index a7e3c2478c..64e8da9201 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -12,7 +12,7 @@ .light = pluralize(commits.count, 'commit') .col-md-10.col-sm-12 - %ul.bordered-list + %ul.content-list = render commits, project: project %hr.lists-separator diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 5e182af266..f6565f8583 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| - -if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2e1a37aa06..eaab99973a 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs, diff_refs) -.content-block.oneline-block +.content-block.oneline-block.files-changed .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 698ed02ea0..83a8d7ae9b 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -3,7 +3,7 @@ - if diff_file.diff.submodule? %span = icon('archive fw') - %strong + %span = submodule_link(blob, @commit.id, project.repository) - else = blob_icon blob.mode, blob.name @@ -11,13 +11,13 @@ = link_to "#diff-#{i}" do - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.filename.old + .filename.old = old_path → - %strong.filename.new + .filename.new = new_path - else - %strong + %span = diff_file.new_path - if diff_file.deleted_file deleted @@ -28,8 +28,8 @@ .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file" do - = icon('comments') + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do + = icon('comment') \ - if editable_diff?(diff_file) diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 8367112a9c..2731219cca 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,7 +1,10 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_commit_id = diff_refs.first.id -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +// diff_refs will be nil for orphaned commits (e.g. first commit in repo) +- if diff_refs + - old_commit_id = diff_refs.first.id + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) + - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 905f6bbbd4..1fe1d98bf1 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -2,7 +2,7 @@ - header_title project_title(@project, "Files", project_files_path(@project)) .file-finder-holder.tree-holder.clearfix - .gray-content-block.top-block + .nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'find_file', path: @path %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 67d016bd87..e39224d86c 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -9,10 +9,8 @@ %hr.clearfix = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index e66e4669d4..6da8e4f33a 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,5 +1,5 @@ - if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) .pull-right - = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do + = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name do = icon('code-fork') New Branch diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index be7a0bb562..aa143e54ff 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 0612863296..097a65969a 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,24 +1,27 @@ %li{id: dom_id(label)} = render "shared/label_row", label: label - .pull-right - %strong.append-right-20 + .pull-info-right + %span.append-right-20 = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'open merge request' + = pluralize label.open_merge_requests_count, 'merge request' - %strong.append-right-20 + %span.append-right-20 = link_to_label(label) do = pluralize label.open_issues_count(current_user), 'open issue' - if current_user .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %button.btn.btn-sm.btn-info.subscribe-button + + %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' - = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do + %i.fa.fa-pencil-square-o + = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do + %i.fa.fa-trash-o - if current_user :javascript diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 01dc7519be..7d7c487e97 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -5,33 +5,74 @@ .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 - .panel.panel-default + .panel.panel-default.panel-new-merge-request .panel-heading - %strong Source branch - .panel-body - = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true }) -   - = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } }) + Source branch + .panel-body.clearfix + .merge-request-select.dropdown + = f.hidden_field :source_project_id + = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" } + .dropdown-menu.dropdown-menu-selectable.dropdown-source-project + = dropdown_title("Select source project") + = dropdown_filter("Search projects") + = dropdown_content do + - is_active = f.object.source_project_id == @merge_request.source_project.id + %ul + %li + %a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } } + = @merge_request.source_project_path + .merge-request-select.dropdown + = f.hidden_field :source_branch + = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } + .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch + = dropdown_title("Select source branch") + = dropdown_filter("Search branches") + = dropdown_content do + %ul + - @merge_request.source_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } } + = branch .panel-footer - .mr_source_commit + = icon('spinner spin', class: 'js-source-loading') + %ul.list-unstyled.mr_source_commit .col-md-6 - .panel.panel-default + .panel.panel-default.panel-new-merge-request .panel-heading - %strong Target branch - .panel-body + Target branch + .panel-body.clearfix - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true }) -   - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } }) + .merge-request-select.dropdown + = f.hidden_field :target_project_id + = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } + .dropdown-menu.dropdown-menu-selectable.dropdown-target-project + = dropdown_title("Select target project") + = dropdown_filter("Search projects") + = dropdown_content do + %ul + - projects.each do |project| + %li + %a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } } + = project.path_with_namespace + .merge-request-select.dropdown + = f.hidden_field :target_branch + = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" } + .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown + = dropdown_title("Select target branch") + = dropdown_filter("Search branches") + = dropdown_content do + %ul + - @merge_request.target_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } } + = branch .panel-footer - .mr_target_commit + = icon('spinner spin', class: "js-target-loading") + %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? - .alert.alert-danger - - @merge_request.errors.full_messages.each do |msg| - %div= msg - + = form_errors(@merge_request) - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? .light-well.append-bottom-default .center @@ -45,40 +86,11 @@ and %span.label-branch #{@merge_request.target_branch} are the same. - - - .form-actions - = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" + = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript - var source_branch = $("#merge_request_source_branch") - , target_branch = $("#merge_request_target_branch") - , target_project = $("#merge_request_target_project_id"); - - $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: source_branch.val() }); - $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); - - target_project.on("change", function() { - $.get("#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: $(this).val() }); - }); - source_branch.on("change", function() { - $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: $(this).val() }); - $(".mr-compare-errors").fadeOut(); - $(".mr-compare-btn").enable(); - }); - target_branch.on("change", function() { - $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); - $(".mr-compare-errors").fadeOut(); - $(".mr-compare-btn").enable(); - }); - - -:javascript - $(".merge-request-form").on('submit', function () { - if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") { - $(".mr-compare-errors").html("You must select source and target branch to proceed"); - $(".mr-compare-errors").fadeIn(); - event.preventDefault(); - return; - } + new Compare({ + targetProjectUrl: "#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", + sourceBranchUrl: "#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", + targetBranchUrl: "#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ee5b9fd95a..1dd8f721f7 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -10,7 +10,7 @@ .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" - .merge-request-details.issuable-details + .merge-request-details.issuable-details{data: {id: @merge_request.project.id}} = render "projects/merge_requests/show/mr_box" .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/branch_from.html.haml new file mode 100644 index 0000000000..4f90dde6fa --- /dev/null +++ b/app/views/projects/merge_requests/branch_from.html.haml @@ -0,0 +1 @@ += commit_to_html(@commit, @source_project, false) diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml deleted file mode 100644 index 9210798f39..0000000000 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -:plain - $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); - $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/branch_to.html.haml new file mode 100644 index 0000000000..67a7a6bcec --- /dev/null +++ b/app/views/projects/merge_requests/branch_to.html.haml @@ -0,0 +1 @@ += commit_to_html(@commit, @target_project, false) diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml deleted file mode 100644 index 32fe2d535f..0000000000 --- a/app/views/projects/merge_requests/branch_to.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -:plain - $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}"); - $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/update_branches.html.haml new file mode 100644 index 0000000000..1b93188a10 --- /dev/null +++ b/app/views/projects/merge_requests/update_branches.html.haml @@ -0,0 +1,5 @@ +%ul + - @target_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } } + = branch diff --git a/app/views/projects/merge_requests/update_branches.js.haml b/app/views/projects/merge_requests/update_branches.js.haml deleted file mode 100644 index ca21b3bc0d..0000000000 --- a/app/views/projects/merge_requests/update_branches.js.haml +++ /dev/null @@ -1,9 +0,0 @@ -:plain - $(".target_branch").html("#{escape_javascript(options_for_select(@target_branches))}"); - - $('select.target_branch').select2({ - width: 'resolve', - dropdownAutoWidth: true - }); - - $(".mr_target_commit").html(""); diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index b05ab86921..2ec0d20a87 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,15 +1,17 @@ - if @ci_commit .mr-widget-heading - .ci_widget{class: "ci-#{@ci_commit.status}"} - = ci_status_icon(@ci_commit) - %span - Build - = ci_status_label(@ci_commit) - for - = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - %w[success skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -43,5 +45,5 @@ :javascript $(function() { - merge_request_widget.getCiStatus(); + merge_request_widget.getCIStatus(false); }); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b2..3c68d61c4b 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -8,13 +8,22 @@ = render 'projects/merge_requests/widget/locked' :javascript - var merge_request_widget; - - merge_request_widget = new MergeRequestWidget({ - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + var opts = { + merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", + ci_status: "", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}", - }); + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + }; + merge_request_widget = new MergeRequestWidget(opts); diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 23f2bca7ba..b2dae1c70e 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,9 +1,6 @@ = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| - -if @milestone.errors.any? - .alert.alert-danger - %ul - - @milestone.errors.full_messages.each do |msg| - %li= msg + = form_errors(@milestone) + .row .col-md-6 .form-group diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 2523311213..a4c6094c69 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -19,7 +19,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} .input-group-addon \/ - else diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 11f9859a90..39be072855 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -3,9 +3,6 @@ - if !defined?(line) || line == note.diff_line %tr.notes_holder %td.notes_line{ colspan: 2 } - %span.discussion-notes-count - %i.fa.fa-comment - = notes.count %td.notes_content %ul.notes{ data: { discussion_id: note.discussion_id } } = render notes diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index bb761ed2f9..f8aa5e2fa7 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -4,9 +4,6 @@ %tr.notes_holder - if note1 %td.notes_line.old - %span.btn.disabled - %i.fa.fa-comment - = notes_left.count %td.notes_content.parallel.old %ul.notes{ data: { discussion_id: note1.discussion_id } } = render notes_left @@ -19,9 +16,6 @@ - if note2 %td.notes_line.new - %span.btn.disabled - %i.fa.fa-comment - = notes_right.count %td.notes_content.parallel.new %ul.notes{ data: { discussion_id: note2.discussion_id } } = render notes_right diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 2999befffc..23e4f93eab 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,10 +1,11 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' = render 'projects/notes/hints' .note-form-actions.clearfix = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' - = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel' + %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } + Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index f675f092da..c446ecec2c 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 6e7929bdab..0b00204340 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,9 +1,8 @@ -.comment-hints.clearfix - .pull-left +.comment-toolbar.clearfix + .toolbar-text + Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 - tip: - = random_markdown_tip - .pull-right - = link_to '#', class: 'markdown-selector', tabindex: -1 do - = icon('paperclip') - Attach a file + is supported + %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } + = icon('file-image-o', class: 'toolbar-button-icon') + Attach a file diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 2cf32e6093..5c42423541 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,28 +5,21 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header + = link_to_member(note.project, note.author, avatar: false) + .inline.note-headline-light + = "#{note.author.to_reference} commented" + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - if note_editable?(note) .note-actions - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do - = icon('pencil-square-o') - - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil') + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') - - - unless note.system - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role.label - = access - - = link_to_member(note.project, note.author, avatar: false) - - %span.author-username - = '@' + note.author.username - - %span.note-last-update - %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 910eb6cf66..cc42aab5c5 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,20 +1,21 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" -.js-notes-busy - -.js-main-target-form -- if can? current_user, :create_note, @project - = render "projects/notes/form", view: diff_view -- else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment +%ul.notes.timeline + %li.timeline-entry + - if can? current_user, :create_note, @project + .timeline-icon.hidden-xs.hidden-sm + %a.author_link{ href: user_path(current_user) } + = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' + .timeline-content.timeline-content-form + = render "projects/notes/form", view: diff_view + - else + .disabled-comment.text-center + .disabled-comment-text.inline + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 4f15a99d06..cd8a5f0bd0 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -1,22 +1,20 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + on the diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 3da2f2060b..46f2ba4bbc 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -1,20 +1,22 @@ - note = discussion_notes.first +- commit = note.noteable +- commit_description = commit ? 'commit' : 'a deleted commit' .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion on #{commit_description}" + - if commit + = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on commit - = link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index 218b0da397..f8e000b424 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -1,19 +1,18 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + on the outdated diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on the - %strong outdated diff - %div + .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index cfd7e1534c..653b02da4d 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -13,11 +13,7 @@ - if can? current_user, :admin_project, @project = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| - -if @protected_branch.errors.any? - .alert.alert-danger - %ul - - @protected_branch.errors.full_messages.each do |msg| - %li= msg + = form_errors(@protected_branch) .form-group = f.label :name, "Branch", class: 'control-label' diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 667057ef2d..093d1d1bb0 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -6,7 +6,7 @@ %span.caret %span.sr-only Select Archive Format - %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.fa.fa-download diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index ba69569b1e..1c5f8b3928 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -15,11 +15,11 @@ - if current_user %li - 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' }} + %span.btn.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"} + %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu - if can_edit_tree? diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index efe1e6f24c..ca284b84d3 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -13,13 +13,7 @@ = nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - - if @project.errors.any? - #error_explanation - %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg + = form_errors(@project) = f.fields_for :variables do |variable_form| .form-group diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index f0d1932e23..812876e283 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| - -if @page.errors.any? - #error_explanation - .alert.alert-danger - - @page.errors.full_messages.each do |msg| - %p= msg + = form_errors(@page) = f.hidden_field :title, value: @page.title .form-group diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 5fcba2b7e9..9544e3d3e1 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,24 +1,20 @@ - project = note.project +- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on + = link_to project.name_with_namespace, project + · - if note.for_commit? - = link_to project do - = project.name_with_namespace - · - = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do - Commit #{truncate_sha(note.commit_id)} + = link_to "Commit #{truncate_sha(note.commit_id)}", note_url - else - = link_to project do - = project.name_with_namespace + %span #{note.noteable_type.titleize} ##{noteable_identifier} · - %span #{note.noteable_type.titleize} ##{note.noteable.iid} - · - = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do - = note.noteable.title + = link_to note.noteable.title, note_url .note-search-result .term diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 7afbaeddee..0a38327baa 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -6,7 +6,7 @@ .commit-message-container .max-width-marker = text_area_tag 'commit_message', - (params[:commit_message] || local_assigns[:text]), + (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 4b47b0291b..b38c5e18ef 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,4 +1,5 @@ %span.label-row - = link_to_label(label, tooltip: false) + %span.label-name + = link_to_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 5a60ff5a5d..fc935166bf 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -1,9 +1,4 @@ -- if @service.errors.any? - #error_explanation - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg += form_errors(@service) - if @service.help.present? .well diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 66b7ef9965..40c6eb9be4 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -21,7 +21,7 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) = image_tag group_icon(group), class: "avatar s40 hidden-xs" diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 53952e608e..921eaefd79 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -7,15 +7,15 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline - - if params[:author_id] + - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", + = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - - if params[:assignee_id] + - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", + = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter @@ -23,7 +23,6 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" - .pull-right = render 'shared/sort_dropdown' @@ -31,18 +30,17 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do .filter-item.inline - = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li %a{href: "#", data: {id: "reopen"}} Open %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b01a36265f..757a3812de 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,10 +1,5 @@ -- if issuable.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - issuable.errors.full_messages.each do |msg| - %span= msg - %br += form_errors(issuable) + .form-group = f.label :title, class: 'control-label' .col-sm-10 @@ -14,7 +9,7 @@ - if issuable.is_a?(MergeRequest) %p.help-block .js-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Remove the %code WIP: prefix from the title @@ -22,7 +17,7 @@ %strong Work In Progress merge request to be merged when it's ready. .js-no-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Start the title with %code WIP: to prevent a @@ -53,10 +48,11 @@ .issue-assignee = f.label :assignee_id, "Assignee", class: 'control-label' .col-sm-10 - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) + .issuable-form-select-holder + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true, include_blank: true)   = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group @@ -64,8 +60,9 @@ = f.label :milestone_id, "Milestone", class: 'control-label' .col-sm-10 - if milestone_options(issuable).present? - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) + .issuable-form-select-holder + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - else .prepend-top-10 %span.light No open milestones available. @@ -127,7 +124,7 @@ for this project. - if issuable.new_record? - = link_to 'Cancel', namespace_project_issues_path(@project.namespace, @project), class: 'btn btn-cancel' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) @@ -135,4 +132,4 @@ method: :delete, class: 'btn btn-grouped' do = icon('trash-o') Delete - = link_to 'Cancel', namespace_project_issue_path(@project.namespace, @project, issuable), class: 'btn btn-grouped btn-cancel' + = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 186087e8f8..fd5e58c1f1 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,4 +1,4 @@ -- if params[:label_name] +- if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} @@ -24,17 +24,21 @@ - else View labels - if can? current_user, :admin_label, @project and @project - .dropdown-page-two + .dropdown-page-two.dropdown-new-label = dropdown_title("Create new label", back: true) = dropdown_content do .dropdown-labels-error.js-label-error - %input#new_label_color{type: "hidden"} %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} - .dropdown-label-color-preview.js-dropdown-label-color-preview .suggest-colors.suggest-colors-dropdown - suggested_colors.each do |color| = link_to '#', style: "background-color: #{color}", data: { color: color } do   - %button.btn.btn-primary.js-new-label-btn{type: "button"} - Create + .dropdown-label-color-input + .dropdown-label-color-preview.js-dropdown-label-color-preview + %input#new_label_color.dropdown-input-field{ type: "text" } + .clearfix + %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"} + Create + %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"} + Cancel = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 0434506c8d..2fcf40ece9 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,7 +1,7 @@ -- if params[:milestone_title] +- if params[:milestone_title].present? = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do += dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if @project %ul.dropdown-footer-list - if can? current_user, :admin_milestone, @project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 5b2772de3f..94affa4b59 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,6 @@ %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar + - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header %span.issuable-count.hide-collapsed.pull-left = issuable.iid @@ -28,7 +29,8 @@ = icon('user') .title.hide-collapsed Assignee - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = icon('spinner spin', class: 'block-loading') + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - if issuable.assignee @@ -39,10 +41,15 @@ %span.username = issuable.assignee.to_reference - else - .light None + %span.assign-yourself + No assignee + - if can_edit_issuable + %a.js-assign-yourself{ href: '#' } + \- assign yourself .selectbox.hide-collapsed - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) + = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon @@ -54,7 +61,8 @@ No .title.hide-collapsed Milestone - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = icon('spinner spin', class: 'block-loading') + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - if issuable.milestone @@ -62,10 +70,10 @@ = issuable.milestone.title - else .light None + .selectbox.hide-collapsed - = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) - = hidden_field_tag :issuable_context - = f.submit class: 'btn hide' + = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil + = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - if issuable.project.labels.any? .block.labels @@ -75,17 +83,41 @@ = issuable.labels.count .title.hide-collapsed Labels - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = icon('spinner spin', class: 'block-loading') + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)} + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - if issuable.labels.any? - issuable.labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - = 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" } + - issuable.labels.each do |label| + = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %span.dropdown-toggle-text + Label + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + = dropdown_title("Assign labels") + = dropdown_filter("Search labels") + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -116,5 +148,8 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new Subscription('.subscription'); - new IssuableContext(); + new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); + new LabelsSelect(); + new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); + new Subscription('.subscription') + new Sidebar(); \ No newline at end of file diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 803dd95bc6..53ff8959bc 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -27,7 +27,7 @@ %span = icon('star') = project.star_count - %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} = visibility_level_icon(project.visibility_level, fw: false) .title diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 1041eccd1d..47ec09f62c 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,10 +1,6 @@ .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| - - if @snippet.errors.any? - .alert.alert-danger - %ul - - @snippet.errors.full_messages.each do |msg| - %li= msg + = form_errors(@snippet) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 7f29918dba..1de71f37d1 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -7,4 +7,4 @@ '#{user_calendar_activities_path}' ); -.calendar-hint Summary of issues, merge requests and push events +.calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index bca816f22c..0c4b6a5618 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -87,7 +87,7 @@ %div{ class: container_class } .tab-content #activity.tab-pane - .gray-content-block.white.second-block + .gray-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } .user-calendar{data: {href: user_calendar_path}} %h4.center.light diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 0264722977..dc249155b9 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,7 +1,7 @@ .awards.votes-block - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}} - = emoji_icon(emoji) + %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} + = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter = notes.count @@ -15,12 +15,14 @@ - if current_user :javascript + var get_emojis_url = "#{emojis_path}"; var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; var noteable_type = "#{votable.class.name.underscore}"; var noteable_id = "#{votable.id}"; var aliases = #{AwardEmoji.aliases.to_json}; window.awards_handler = new AwardsHandler( + get_emojis_url, post_emoji_url, noteable_type, noteable_id, diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 55cb6af232..ccefd0f71a 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -5,6 +5,9 @@ class ProjectCacheWorker def perform(project_id) project = Project.find(project_id) + + return unless project.repository.exists? + project.update_repository_size project.update_commit_count diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index d06e448029..b51c6a266c 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -5,7 +5,7 @@ class ProjectDestroyWorker def perform(project_id, user_id, params) begin - project = Project.find(project_id) + project = Project.unscoped.find(project_id) rescue ActiveRecord::RecordNotFound return end diff --git a/config/application.rb b/config/application.rb index 5a0ac70aa2..2e2ed48db0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,11 +4,9 @@ require 'rails/all' require 'devise' I18n.config.enforce_available_locales = false Bundler.require(:default, Rails.env) -require_relative '../lib/gitlab/redis_config' +require_relative '../lib/gitlab/redis' module Gitlab - REDIS_CACHE_NAMESPACE = 'cache:gitlab' - class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -69,8 +67,8 @@ module Gitlab end end - redis_config_hash = Gitlab::RedisConfig.redis_store_options - redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE + redis_config_hash = Gitlab::Redis.redis_store_options + redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/environments/production.rb b/config/environments/production.rb index 909526605a..a9d8ac4b6d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -21,6 +21,9 @@ Rails.application.configure do # Generate digests for assets URLs config.assets.digest = true + # Enable compression of compiled assets using gzip. + config.assets.compress = true + # Defaults to nil and saved in location specified by config.assets.prefix # config.assets.manifest = YOUR_PATH diff --git a/config/environments/test.rb b/config/environments/test.rb index f96ac6f975..a703c0934f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,6 +8,7 @@ Rails.application.configure do config.cache_classes = false # Configure static asset server for tests with Cache-Control for performance + config.assets.digest = false config.serve_static_files = true config.static_cache_control = "public, max-age=3600" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 500b745f55..35c7c425a5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -80,7 +80,7 @@ production: &base # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. # Tip: you can test your closing pattern at http://rubular.com. - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' ## Default project features settings default_projects_features: @@ -106,7 +106,7 @@ production: &base enabled: false # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "gitlab-incoming+%{key}@gmail.com" # Email account username @@ -345,6 +345,8 @@ production: &base # # - { name: 'saml', # label: 'Our SAML Provider', + # groups_attribute: 'Groups', + # external_groups: ['Contractors', 'Freelancers'], # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', @@ -352,6 +354,7 @@ production: &base # issuer: 'https://gitlab.example.com', # name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' # } } + # # - { name: 'crowd', # args: { # crowd_server_url: 'CROWD SERVER URL', diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 626268d764..72c4d8d61c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -174,10 +174,9 @@ end Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? -Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 3e1deb8d30..22fe51a453 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -1,4 +1,5 @@ if Gitlab::Metrics.enabled? + require 'pathname' require 'influxdb' require 'connection_pool' require 'method_source' @@ -7,6 +8,7 @@ if Gitlab::Metrics.enabled? # ActiveSupport. require 'gitlab/metrics/subscribers/action_view' require 'gitlab/metrics/subscribers/active_record' + require 'gitlab/metrics/subscribers/rails_cache' Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) @@ -74,6 +76,37 @@ if Gitlab::Metrics.enabled? config.instrument_methods(const) config.instrument_instance_methods(const) end + + # Instruments all Banzai filters + Dir[Rails.root.join('lib', 'banzai', 'filter', '*.rb')].each do |file| + klass = File.basename(file, File.extname(file)).camelize + const = Banzai::Filter.const_get(klass) + + config.instrument_methods(const) + config.instrument_instance_methods(const) + end + + config.instrument_methods(Banzai::Renderer) + config.instrument_methods(Banzai::Querying) + + [Issuable, Mentionable, Participable].each do |klass| + config.instrument_instance_methods(klass) + config.instrument_instance_methods(klass::ClassMethods) + end + + config.instrument_methods(Gitlab::ReferenceExtractor) + config.instrument_instance_methods(Gitlab::ReferenceExtractor) + + # Instrument all service classes + services = Rails.root.join('app', 'services') + + Dir[services.join('**', '*.rb')].each do |file_path| + path = Pathname.new(file_path).relative_path_from(services) + const = path.to_s.sub('.rb', '').camelize.constantize + + config.instrument_methods(const) + config.instrument_instance_methods(const) + end end GC::Profiler.enable diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index a44316bc3a..b9176688bc 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,5 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true + remove_ids: true, + remove_scripts: false ) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 3da5d46be9..7028525587 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,7 +13,7 @@ end if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else - redis_config = Gitlab::RedisConfig.redis_store_options + redis_config = Gitlab::Redis.redis_store_options redis_config[:namespace] = 'session:gitlab' Gitlab::Application.config.session_store( diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index cc83137745..9182d92980 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -2,7 +2,7 @@ SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' Sidekiq.configure_server do |config| config.redis = { - url: Gitlab::RedisConfig.url, + url: Gitlab::Redis.url, namespace: SIDEKIQ_REDIS_NAMESPACE } @@ -29,7 +29,7 @@ end Sidekiq.configure_client do |config| config.redis = { - url: Gitlab::RedisConfig.url, + url: Gitlab::Redis.url, namespace: SIDEKIQ_REDIS_NAMESPACE } end diff --git a/config/mail_room.yml b/config/mail_room.yml index aed55f74ea..761a32adb9 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,7 +2,7 @@ <% require "yaml" require "json" -require_relative "lib/gitlab/redis_config" +require_relative "lib/gitlab/redis" rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" @@ -17,8 +17,8 @@ if File.exists?(config_file) config['start_tls'] = false if config['start_tls'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil? - if config['enabled'] && config['address'] && config['address'].include?('%{key}') - redis_url = Gitlab::RedisConfig.new(rails_env).url + if config['enabled'] && config['address'] + redis_url = Gitlab::Redis.new(rails_env).url %> - :host: <%= config['host'].to_json %> diff --git a/config/routes.rb b/config/routes.rb index 40c149abda..f8a8c9bc7b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,18 @@ Rails.application.routes.draw do end end + # Make the built-in Rails routes available in development, otherwise they'd + # get swallowed by the `namespace/project` route matcher below. + # + # See https://git.io/va79N + if Rails.env.development? + get '/rails/mailers' => 'rails/mailers#index' + get '/rails/mailers/:path' => 'rails/mailers#preview' + get '/rails/info/properties' => 'rails/info#properties' + get '/rails/info/routes' => 'rails/info#routes' + get '/rails/info' => 'rails/info#index' + end + namespace :ci do # CI API Ci::API::API.logger Rails.logger @@ -570,6 +582,7 @@ Rails.application.routes.draw do # Order matters to give priority to these matches get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' + post '/wikis/markdown_preview', to:'wikis#markdown_preview' post '/wikis', to: 'wikis#create' get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID @@ -744,10 +757,11 @@ Rails.application.routes.draw do end resources :runner_projects, only: [:create, :destroy] - resources :badges, only: [], path: 'badges/*ref', - constraints: { ref: Gitlab::Regex.git_reference_regex } do + resources :badges, only: [:index] do collection do - get :build, constraints: { format: /svg/ } + scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do + get :build, constraints: { format: /svg/ } + end end end end diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index e028ac82ba..540e4e6825 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do milestone_params = { title: "v#{i}.0", description: FFaker::Lorem.sentence, - state: ['opened', 'closed'].sample, + state: [:active, :closed].sample, } milestone = Milestones::CreateService.new( diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb index 9fa96203ff..99289166e8 100644 --- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb +++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb @@ -1,14 +1,18 @@ class ConvertClosedToStateInIssue < ActiveRecord::Migration + include Gitlab::Database + def up - Issue.transaction do - Issue.where(closed: true).update_all(state: :closed) - Issue.where(closed: false).update_all(state: :opened) - end + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}" + execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}" end def down - Issue.transaction do - Issue.where(state: :closed).update_all(closed: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'" + end + + private + + def table_name + Issue.table_name end end diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb index ebb7ae585e..bd1e016d67 100644 --- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb +++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb @@ -1,16 +1,20 @@ class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration + include Gitlab::Database + def up - MergeRequest.transaction do - MergeRequest.where(closed: true, merged: true).update_all(state: :merged) - MergeRequest.where(closed: true, merged: false).update_all(state: :closed) - MergeRequest.where(closed: false).update_all(state: :opened) - end + execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}" + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}" + execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}" end def down - MergeRequest.transaction do - MergeRequest.where(state: :closed).update_all(closed: true) - MergeRequest.where(state: :merged).update_all(closed: true, merged: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'" + execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'" + end + + private + + def table_name + MergeRequest.table_name end end diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb index 1978ea8915..d1174bc3d9 100644 --- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb +++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb @@ -1,14 +1,18 @@ class ConvertClosedToStateInMilestone < ActiveRecord::Migration + include Gitlab::Database + def up - Milestone.transaction do - Milestone.where(closed: true).update_all(state: :closed) - Milestone.where(closed: false).update_all(state: :active) - end + execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}" + execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}" end def down - Milestone.transaction do - Milestone.where(state: :closed).update_all(closed: true) - end + execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'" + end + + private + + def table_name + Milestone.table_name end end diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb index b310b35e37..1c758c56ff 100644 --- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb +++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb @@ -1,17 +1,19 @@ class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration def up - MergeRequest.transaction do - MergeRequest.where(merge_status: 1).update_all("new_merge_status = 'unchecked'") - MergeRequest.where(merge_status: 2).update_all("new_merge_status = 'can_be_merged'") - MergeRequest.where(merge_status: 3).update_all("new_merge_status = 'cannot_be_merged'") - end + execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1" + execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2" + execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3" end def down - MergeRequest.transaction do - MergeRequest.where(new_merge_status: :unchecked).update_all("merge_status = 1") - MergeRequest.where(new_merge_status: :can_be_merged).update_all("merge_status = 2") - MergeRequest.where(new_merge_status: :cannot_be_merged).update_all("merge_status = 3") - end + execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'" + execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'" + execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'" + end + + private + + def table_name + MergeRequest.table_name end end diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb index fe139e32ea..56c9a31ee3 100644 --- a/db/migrate/20130315124931_user_color_scheme.rb +++ b/db/migrate/20130315124931_user_color_scheme.rb @@ -1,7 +1,9 @@ class UserColorScheme < ActiveRecord::Migration + include Gitlab::Database + def up add_column :users, :color_scheme_id, :integer, null: false, default: 1 - User.where(dark_scheme: true).update_all(color_scheme_id: 2) + execute("UPDATE users SET color_scheme_id = 2 WHERE dark_scheme = #{true_value}") remove_column :users, :dark_scheme end diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb index 2a036bd999..85e31608d7 100644 --- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb +++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb @@ -3,14 +3,16 @@ class AddLastActivityColumnIntoProject < ActiveRecord::Migration add_column :projects, :last_activity_at, :datetime add_index :projects, :last_activity_at - Project.find_each do |project| - last_activity_date = if project.last_activity - project.last_activity.created_at - else - project.updated_at - end + select_all('SELECT id, updated_at FROM projects').each do |project| + project_id = project['id'] + update_date = project['updated_at'] + event = select_one("SELECT created_at FROM events WHERE project_id = #{project_id} ORDER BY created_at DESC LIMIT 1") - project.update_attribute(:last_activity_at, last_activity_date) + if event && event['created_at'] + update_date = event['created_at'] + end + + execute("UPDATE projects SET last_activity_at = '#{update_date}' WHERE id = #{project_id}") end end diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb index 56ce58a846..56ea97e856 100644 --- a/db/migrate/20130419190306_allow_merges_for_forks.rb +++ b/db/migrate/20130419190306_allow_merges_for_forks.rb @@ -1,7 +1,7 @@ class AllowMergesForForks < ActiveRecord::Migration def self.up add_column :merge_requests, :target_project_id, :integer, :null => true - MergeRequest.update_all("target_project_id = project_id") + execute "UPDATE #{table_name} SET target_project_id = project_id" change_column :merge_requests, :target_project_id, :integer, :null => false rename_column :merge_requests, :project_id, :source_project_id end @@ -10,4 +10,10 @@ class AllowMergesForForks < ActiveRecord::Migration remove_column :merge_requests, :target_project_id rename_column :merge_requests, :source_project_id,:project_id end + + private + + def table_name + MergeRequest.table_name + end end diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb index cf1e9f912a..89421cbeda 100644 --- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb +++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb @@ -1,13 +1,15 @@ class AddVisibilityLevelToProjects < ActiveRecord::Migration + include Gitlab::Database + def self.up add_column :projects, :visibility_level, :integer, :default => 0, :null => false - Project.where(public: true).update_all(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + execute("UPDATE projects SET visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} WHERE public = #{true_value}") remove_column :projects, :public end def self.down add_column :projects, :public, :boolean, :default => false, :null => false - Project.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).update_all(public: true) + execute("UPDATE projects SET public = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}") remove_column :projects, :visibility_level end end diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb index f4392c0f05..0a9f73a575 100644 --- a/db/migrate/20140313092127_migrate_already_imported_projects.rb +++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb @@ -1,12 +1,14 @@ class MigrateAlreadyImportedProjects < ActiveRecord::Migration + include Gitlab::Database + def up - Project.where(imported: true).update_all(import_status: "finished") - Project.where(imported: false).update_all(import_status: "none") + execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}") + execute("UPDATE projects SET import_status = 'none' WHERE imported = #{false_value}") remove_column :projects, :imported end def down add_column :projects, :imported, :boolean, default: false - Project.where(import_status: 'finished').update_all(imported: true) + execute("UPDATE projects SET imported = #{true_value} WHERE import_status = 'finished'") end end diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb index 7f125acb5d..93826185e8 100644 --- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -1,9 +1,11 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration + include Gitlab::Database + def up add_column :snippets, :visibility_level, :integer, :default => 0, :null => false - Snippet.where(private: true).update_all(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - Snippet.where(private: false).update_all(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::PRIVATE} WHERE private = #{true_value}") + execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::INTERNAL} WHERE private = #{false_value}") add_index :snippets, :visibility_level @@ -12,10 +14,10 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration def down add_column :snippets, :private, :boolean, :default => false, :null => false - - Snippet.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).update_all(private: false) - Snippet.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).update_all(private: true) - + + execute("UPDATE snippets SET private = #{false_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::INTERNAL}") + execute("UPDATE snippets SET private = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PRIVATE}") + remove_column :snippets, :visibility_level end end diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb new file mode 100644 index 0000000000..1fff9759d1 --- /dev/null +++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedIssues < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'Issue' + AND NOT EXISTS ( + SELECT * + FROM issues + WHERE issues.id = todos.target_id + AND issues.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb new file mode 100644 index 0000000000..275554e736 --- /dev/null +++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb @@ -0,0 +1,6 @@ +class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration + def change + add_index :projects, :pending_delete + end +end + diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb new file mode 100644 index 0000000000..54cea964ff --- /dev/null +++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'MergeRequest' + AND NOT EXISTS ( + SELECT * + FROM merge_requests + WHERE merge_requests.id = todos.target_id + AND merge_requests.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb new file mode 100644 index 0000000000..0d736e323b --- /dev/null +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -0,0 +1,5 @@ +class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :twitter_sharing_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index dce2bfe62c..ec235c1913 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: 20160320204112) do +ActiveRecord::Schema.define(version: 20160331223143) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,7 +44,6 @@ ActiveRecord::Schema.define(version: 20160320204112) do t.datetime "updated_at" t.string "home_page_url" 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 @@ -418,7 +417,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do t.integer "iid" t.integer "updated_by_id" t.integer "moved_to_id" - t.boolean "confidential", default: false + t.boolean "confidential", default: false t.datetime "deleted_at" end @@ -745,6 +744,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} + add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree diff --git a/doc/README.md b/doc/README.md index 08d0a6a5bf..724c7cca0f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. -- [CI](ci/README.md) +- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [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). @@ -19,10 +19,12 @@ ## Administrator documentation +- [Authentication/Authorization](administration/auth/README.md) Configure + external authentication with LDAP, SAML, CAS and additional Omniauth providers. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components -- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. +- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [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. @@ -45,4 +47,3 @@ contributing to documentation. - [Development](development/README.md) Explains the architecture and the guidelines for shell commands. - [Legal](legal/README.md) Contributor license agreements. -- [Release](release/README.md) How to make the monthly and security releases. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md new file mode 100644 index 0000000000..07e548aaab --- /dev/null +++ b/doc/administration/auth/README.md @@ -0,0 +1,11 @@ +# Authentication and Authorization + +GitLab integrates with the following external authentication and authorization +providers. + +- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, + and 389 Server +- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, + Bitbucket, Facebook, Shibboleth, Crowd and Azure +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md new file mode 100644 index 0000000000..1009677984 --- /dev/null +++ b/doc/administration/auth/ldap.md @@ -0,0 +1,277 @@ +# LDAP + +GitLab integrates with LDAP to support user authentication. +This integration works with most LDAP-compliant directory +servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP, +and 389 Server. GitLab EE includes enhanced integration, including group +membership syncing. + +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' +or 'userPrincipalName' attribute. An LDAP user who is allowed to change their +email on the LDAP server can potentially +[take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) +on your GitLab server. + +We recommend against using LDAP integration if your LDAP users are +allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on +the LDAP server. + +### User deletion + +If a user is deleted from the LDAP server, they will be blocked in GitLab, as +well. Users will be immediately blocked from logging in. However, there is an +LDAP check cache time (sync time) of one hour (see note). This means users that +are already logged in or are using Git over SSH will still be able to access +GitLab for up to one hour. Manually block the user in the GitLab Admin area to +immediately block all access. + +>**Note**: GitLab EE supports a configurable sync time, with a default +of one hour. + +## Configuration + +To enable LDAP integration you need to add your LDAP server settings in +`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. + +>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to +one GitLab server. + +Prior to version 7.4, GitLab used a different syntax for configuring +LDAP integration. The old LDAP integration syntax still works but may be +removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains +LDAP settings in both the old syntax and the new syntax, only the __old__ +syntax will be used by GitLab. + +The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to +incorrect indentation. Be sure to retain the indentation given in the example. +Copy/paste can sometimes cause problems. + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_enabled'] = true +gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below +main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 389 + uid: 'sAMAccountName' + method: 'plain' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking + # a request if the LDAP server becomes unresponsive. + # A value of 0 means there is no timeout. + timeout: 10 + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + + # LDAP attributes that GitLab will use to create an account for the LDAP user. + # The specified attribute can either be the attribute name as a string (e.g. 'mail'), + # or an array of attribute names to try in order (e.g. ['mail', 'email']). + # Note that the user's LDAP login will always be the attribute specified as `uid` above. + attributes: + # The username will be used in paths for the user's own projects + # (like `gitlab.example.com/username/project`) and when mentioning + # them in issues, merge request and comments (like `@username`). + # If the attribute specified for `username` contains an email address, + # the GitLab username will be the part of the email address before the '@'. + username: ['uid', 'userid', 'sAMAccountName'] + email: ['mail', 'email', 'userPrincipalName'] + + # If no full name could be found at the attribute specified for `name`, + # the full name is determined using the attributes specified for + # `first_name` and `last_name`. + name: 'cn' + first_name: 'givenName' + last_name: 'sn' + + ## EE only + + # Base where we can search for groups + # + # Ex. ou=groups,dc=gitlab,dc=example + # + group_base: '' + + # The CN of a group containing GitLab administrators + # + # Ex. administrators + # + # Note: Not `cn=administrators` or the full DN + # + admin_group: '' + + # The LDAP attribute containing a user's public SSH key + # + # Ex. ssh_public_key + # + sync_ssh_keys: false + +# GitLab EE only: add more LDAP servers +# Choose an ID made of a-z and 0-9 . This ID will be stored in the database +# so that GitLab can remember which LDAP server a user belongs to. +# uswest2: +# label: +# host: +# .... +EOS +``` + +**Source configuration** + +Use the same format as `gitlab_rails['ldap_servers']` for the contents under +`servers:` in the example below: + +``` +production: + # snip... + ldap: + enabled: false + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + # snip... +``` + +## Using an LDAP filter to limit access to your GitLab server + +If you want to limit all GitLab access to a subset of the LDAP users on your +LDAP server, the first step should be to narrow the configured `base`. However, +it is sometimes necessary to filter users further. In this case, you can set up +an LDAP user filter. The filter must comply with +[RFC 4515](https://tools.ietf.org/search/rfc4515). + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_servers'] = YAML.load <<-EOS +main: + # snip... + user_filter: '(employeeType=developer)' +EOS +``` + +**Source configuration** + +```yaml +production: + ldap: + servers: + main: + # snip... + user_filter: '(employeeType=developer)' +``` + +Tip: If you want to limit access to the nested members of an Active Directory +group you can use the following syntax: + +``` +(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) +``` + +Please note that GitLab does not support the custom filter syntax used by +omniauth-ldap. + +## Enabling LDAP sign-in for existing GitLab users + +When a user signs in to GitLab with LDAP for the first time, and their LDAP +email address is the primary email address of an existing GitLab user, then +the LDAP DN will be associated with the existing user. If the LDAP email +attribute is not found in GitLab's database, a new user is created. + +In other words, if an existing GitLab user wants to enable LDAP sign-in for +themselves, they should check that their GitLab email address matches their +LDAP email address, and then sign into GitLab via their LDAP credentials. + +## Limitations + +### TLS Client Authentication + +Not implemented by `Net::LDAP`. +You should disable anonymous LDAP authentication and enable simple or SASL +authentication. The TLS client authentication setting in your LDAP server cannot +be mandatory and clients cannot be authenticated with the TLS protocol. + +### TLS Server Authentication + +Not supported by GitLab's configuration options. +When setting `method: ssl`, the underlying authentication method used by +`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with +the LDAP server before any LDAP-protocol data is exchanged but no validation of +the LDAP server's SSL certificate is performed. + +## Troubleshooting + +### Invalid credentials when logging in + +- Make sure the user you are binding with has enough permissions to read the user's +tree and traverse it. +- Check that the `user_filter` is not blocking otherwise valid users. +- Run the following check command to make sure that the LDAP settings are + correct and GitLab can see your users: + + ```bash + # For Omnibus installations + sudo gitlab-rake gitlab:ldap:check + + # For installations from source + sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production + ``` + +### Connection Refused + +If you are getting 'Connection Refused' errors when trying to connect to the +LDAP server please double-check the LDAP `port` and `method` settings used by +GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR +`method: 'ssl'` and `port: 636`. diff --git a/doc/api/issues.md b/doc/api/issues.md index 18d64c4198..1c635a6cdc 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -76,8 +76,9 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [] - }, + "labels" : [], + "subscribed" : false + } ] ``` @@ -152,7 +153,8 @@ Example response: "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", - "created_at" : "2016-01-04T15:31:46.176Z" + "created_at" : "2016-01-04T15:31:46.176Z", + "subscribed" : false } ] ``` @@ -213,7 +215,8 @@ Example response: "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", - "created_at" : "2016-01-04T15:31:46.176Z" + "created_at" : "2016-01-04T15:31:46.176Z", + "subscribed": false } ``` @@ -237,6 +240,7 @@ POST /projects/:id/issues | `assignee_id` | integer | no | The ID of a user to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | +| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | ```bash curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug @@ -266,7 +270,8 @@ Example response: }, "description" : null, "updated_at" : "2016-01-07T12:44:33.959Z", - "milestone" : null + "milestone" : null, + "subscribed" : true } ``` @@ -322,7 +327,8 @@ Example response: ], "id" : 85, "assignee" : null, - "milestone" : null + "milestone" : null, + "subscribed" : true } ``` diff --git a/doc/api/labels.md b/doc/api/labels.md index 6496ffe9fd..3730c07c5a 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -8,9 +8,9 @@ Get all labels for a given project. GET /projects/:id/labels ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels @@ -22,35 +22,43 @@ Example response: [ { "name" : "bug", - "color" : "#d9534f" + "color" : "#d9534f", + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1 }, { "color" : "#d9534f", - "name" : "confirmed" + "name" : "confirmed", + "description": "Confirmed issue", + "open_issues_count": 2, + "closed_issues_count": 5, + "open_merge_requests_count": 0 }, { "name" : "critical", - "color" : "#d9534f" - }, - { - "color" : "#428bca", - "name" : "discussion" + "color" : "#d9534f", + "description": "Criticalissue. Need fix ASAP", + "open_issues_count": 1, + "closed_issues_count": 3, + "open_merge_requests_count": 1 }, { "name" : "documentation", - "color" : "#f0ad4e" + "color" : "#f0ad4e", + "description": "Issue about documentation", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 2 }, { "color" : "#5cb85c", - "name" : "enhancement" - }, - { - "color" : "#428bca", - "name" : "suggestion" - }, - { - "color" : "#f0ad4e", - "name" : "support" + "name" : "enhancement", + "description": "Enhancement proposal", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1 } ] ``` @@ -66,11 +74,12 @@ and 409 if the label already exists. POST /projects/:id/labels ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `name` | string | yes | The name of the label | -| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign | +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the label | +| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign | +| `description` | string | no | The description of the label | ```bash curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" @@ -81,7 +90,8 @@ Example response: ```json { "name" : "feature", - "color" : "#5843AD" + "color" : "#5843AD", + "description":null } ``` @@ -97,10 +107,10 @@ In case of an error, an additional error message is returned. DELETE /projects/:id/labels ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `name` | string | yes | The name of the label | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the label | ```bash curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" @@ -112,6 +122,7 @@ Example response: { "title" : "feature", "color" : "#5843AD", + "description": "New feature proposal", "updated_at" : "2015-11-03T21:22:30.737Z", "template" : false, "project_id" : 1, @@ -133,15 +144,16 @@ In case of an error, an additional error message is returned. PUT /projects/:id/labels ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `name` | string | yes | The name of the existing label | -| `new_name` | string | yes if `color` if not provided | The new name of the label | -| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer | yes | The ID of the project | +| `name` | string | yes | The name of the existing label | +| `new_name` | string | yes if `color` if not provided | The new name of the label | +| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | +| `description` | string | no | The new description of the label | ```bash -curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" +curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" ``` Example response: @@ -149,6 +161,7 @@ Example response: ```json { "color" : "#8E44AD", - "name" : "docs" + "name" : "docs", + "description": "Documentation" } ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index b20a6300b7..20db73ea6c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -66,7 +66,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : false } ] ``` @@ -128,7 +129,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -227,6 +229,7 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", + "subscribed" : true, "changes": [ { "old_path": "VERSION", @@ -304,7 +307,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -373,7 +377,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -466,7 +471,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -530,7 +536,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` diff --git a/doc/api/milestones.md b/doc/api/milestones.md index a682872826..e4202025f8 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -7,8 +7,24 @@ Returns a list of project milestones. ``` GET /projects/:id/milestones GET /projects/:id/milestones?iid=42 +GET /projects/:id/milestones?state=active +GET /projects/:id/milestones?state=closed ``` +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `iid` | integer | optional | Return only the milestone having the given `iid` | +| `state` | string | optional | Return only `active` or `closed` milestones` | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones +``` + +Example Response: + ```json [ { @@ -25,10 +41,6 @@ GET /projects/:id/milestones?iid=42 ] ``` -Parameters: - -- `id` (required) - The ID of a project -- `iid` (optional) - Return the milestone having the given `iid` ## Get single milestone diff --git a/doc/api/notes.md b/doc/api/notes.md index d4d63e825a..2e0936f11b 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -32,6 +32,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T09:22:45Z", + "updated_at": "2013-10-02T10:22:45Z", "system": true, "upvote": false, "downvote": false, @@ -51,6 +52,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T09:56:03Z", + "updated_at": "2013-10-02T09:56:03Z", "system": true, "upvote": false, "downvote": false, @@ -103,6 +105,53 @@ Parameters: - `note_id` (required) - The ID of a note - `body` (required) - The content of a note +### Delete an issue note + +Deletes an existing note of an issue. On success, this API method returns 200 +and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/issues/:issue_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 +``` + +Example Response: + +```json +{ + "id": 636, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-05T22:10:44.164Z", + "system": false, + "noteable_id": 11, + "noteable_type": "Issue", + "upvote": false, + "downvote": false +} +``` + ## Snippets ### List all snippet notes @@ -180,6 +229,53 @@ Parameters: - `note_id` (required) - The ID of a note - `body` (required) - The content of a note +### Delete a snippet note + +Deletes an existing note of a snippet. On success, this API method returns 200 +and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/snippets/:snippet_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `snippet_id` | integer | yes | The ID of a snippet | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 +``` + +Example Response: + +```json +{ + "id": 1659, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-06T16:51:53.239Z", + "system": false, + "noteable_id": 52, + "noteable_type": "Snippet", + "upvote": false, + "downvote": false +} +``` + ## Merge Requests ### List all merge request notes @@ -223,6 +319,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T08:57:14Z", + "updated_at": "2013-10-02T08:57:14Z", "system": false, "upvote": false, "downvote": false, @@ -259,3 +356,50 @@ Parameters: - `merge_request_id` (required) - The ID of a merge request - `note_id` (required) - The ID of a note - `body` (required) - The content of a note + +### Delete a merge request note + +Deletes an existing note of a merge request. On success, this API method returns +200 and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a merge request | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 +``` + +Example Response: + +```json +{ + "id": 1602, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-05T22:11:59.923Z", + "system": false, + "noteable_id": 7, + "noteable_type": "MergeRequest", + "upvote": false, + "downvote": false +} +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 3703f4b327..ab716c229d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,172 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Archive a project + +Archives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus archiving an already archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + +### Unarchive a project + +Unarchives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus unarchiving an non-archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + ### Remove project Removes a project including all associated resources (issues, merge requests etc.) @@ -614,8 +780,10 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `user_id` (required) - The ID of a team member -This method is idempotent and can be called multiple times with the same parameters. -Revoking team membership for a user who is not currently a team member is considered success. +This method removes the project member if the user has the proper access rights to do so. +It returns a status code 403 if the member does not have the proper rights to perform this action. +In all other cases this method is idempotent and revoking team membership for a user who is not +currently a team member is considered success. Please note that the returned JSON currently differs slightly. Thus you should not rely on the returned JSON structure. diff --git a/doc/api/settings.md b/doc/api/settings.md index 001de76c7a..1e745115dc 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -26,7 +26,6 @@ Example response: "default_branch_protection" : 2, "restricted_visibility_levels" : [], "signin_enabled" : true, - "twitter_sharing_enabled" : true, "after_sign_out_path" : null, "max_attachment_size" : 10, "user_oauth_applications" : true, @@ -57,7 +56,6 @@ PUT /application/settings | `sign_in_text` | string | no | Text on login page | | `home_page_url` | string | no | Redirect to this URL when not logged in | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. | -| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter | | `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | @@ -85,7 +83,6 @@ Example response: "updated_at": "2015-06-30T13:22:42.210Z", "home_page_url": "", "default_branch_protection": 2, - "twitter_sharing_enabled": true, "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, diff --git a/doc/api/tags.md b/doc/api/tags.md index 17d12e9cc6..ac9fac92f4 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -38,6 +38,50 @@ Parameters: ] ``` +## Get a single repository tag + +Get a specific repository tag determined by its name. It returns `200` together +with the tag information if the tag exists. It returns `404` if the tag does not +exist. + +``` +GET /projects/:id/repository/tags/:tag_name +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `tag_name` | string | yes | The name of the tag | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0 +``` + +Example Response: + +```json +{ + "name": "v5.0.0", + "message": null, + "commit": { + "id": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "message": "v5.0.0\n", + "parent_ids": [ + "f61c062ff8bcbdb00e0a1b3317a91aed6ceee06b" + ], + "authored_date": "2015-02-01T21:56:31.000+01:00", + "author_name": "Arthur Verschaeve", + "author_email": "contact@arthurverschaeve.be", + "committed_date": "2015-02-01T21:56:31.000+01:00", + "committer_name": "Arthur Verschaeve", + "committer_email": "contact@arthurverschaeve.be" + }, + "release": null +} +``` + ## Create a new tag Creates a new tag in the repository that points to the supplied ref. @@ -148,4 +192,4 @@ Parameters: "tag_name": "1.0.0", "description": "Amazing release. Wow" } -``` \ No newline at end of file +``` diff --git a/doc/api/users.md b/doc/api/users.md index 383e7c76ab..7d2b4897cf 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -69,6 +69,7 @@ GET /users "state": "blocked", "created_at": "2012-05-23T08:01:01Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -126,6 +127,7 @@ Parameters: "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -154,6 +156,7 @@ Parameters: "confirmed_at": "2012-05-23T08:00:58Z", "last_sign_in_at": "2015-03-23T08:00:58Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", @@ -191,6 +194,7 @@ Parameters: - `extern_uid` (optional) - External UID - `provider` (optional) - External provider name - `bio` (optional) - User's biography +- `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `confirm` (optional) - Require confirmation - true (default) or false @@ -218,6 +222,7 @@ Parameters: - `extern_uid` - External UID - `provider` - External provider name - `bio` - User's biography +- `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) @@ -260,6 +265,7 @@ GET /user "state": "active", "created_at": "2012-05-23T08:00:58Z", "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index 71db5aa5dc..9553bb11e9 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -1,7 +1,10 @@ # Introduction to build artifacts Artifacts is a list of files and directories which are attached to a build -after it completes successfully. +after it completes successfully. This feature is enabled by default in all GitLab installations. + +_If you are searching for ways to use artifacts, jump to +[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._ Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by GitLab Runner are uploaded to GitLab and are downloadable as a single archive @@ -16,13 +19,9 @@ The artifacts browser will be available only for new artifacts that are sent to GitLab using GitLab Runner version 1.0 and up. It will not be possible to browse old artifacts already uploaded to GitLab. -## Enabling build artifacts +## Disabling build artifacts -_If you are searching for ways to use artifacts, jump to -[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._ - -The artifacts feature is enabled by default in all GitLab installations. -To disable it site-wide, follow the steps below. +To disable artifacts site-wide, follow the steps below. --- diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 210f9c3e84..d790015aca 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -57,7 +57,7 @@ before_script: # WARNING: Use this only with the Docker executor, if you use it with shell # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` + - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' ``` As a final step, add the _public_ key from the one you created earlier to the diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4316f3c1f6..7da9b31e30 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -38,7 +38,7 @@ services: - postgres before_script: - - bundle_install + - bundle install stages: - build diff --git a/doc/development/README.md b/doc/development/README.md index 1b281809af..3f3ef068f9 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -2,11 +2,15 @@ - [Architecture](architecture.md) of GitLab - [CI setup](ci_setup.md) for testing GitLab +- [Code review guidelines](code_review.md) for reviewing code and having code + reviewed. - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) +- [Instrumentation](instrumentation.md) - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Rake tasks](rake_tasks.md) for development - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [SQL guidelines](sql.md) for SQL guidelines +- [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/code_review.md b/doc/development/code_review.md new file mode 100644 index 0000000000..40ae55ab90 --- /dev/null +++ b/doc/development/code_review.md @@ -0,0 +1,78 @@ +# Code Review Guidelines + +This guide contains advice and best practices for performing code review, and +having your code reviewed. + +All merge requests for GitLab CE and EE, whether written by a GitLab team member +or a volunteer contributor, must go through a code review process to ensure the +code is effective, understandable, and maintainable. + +Any developer can, and is encouraged to, perform code review on merge requests +of colleagues and contributors. However, the final decision to accept a merge +request is up to one of our merge request "endbosses", denoted on the +[team page](https://about.gitlab.com/team). + +## Everyone + +- Accept that many programming decisions are opinions. Discuss tradeoffs, which + you prefer, and reach a resolution quickly. +- Ask questions; don't make demands. ("What do you think about naming this + `:user_id`?") +- Ask for clarification. ("I didn't understand. Can you clarify?") +- Avoid selective ownership of code. ("mine", "not mine", "yours") +- Avoid using terms that could be seen as referring to personal traits. ("dumb", + "stupid"). Assume everyone is attractive, intelligent, and well-meaning. +- Be explicit. Remember people don't always understand your intentions online. +- Be humble. ("I'm not sure - let's look it up.") +- Don't use hyperbole. ("always", "never", "endlessly", "nothing") +- Be careful about the use of sarcasm. Everything we do is public; what seems + like good-natured ribbing to you and a long-time colleague might come off as + mean and unwelcoming to a person new to the project. +- Consider one-on-one chats or video calls if there are too many "I didn't + understand" or "Alternative solution:" comments. Post a follow-up comment + summarizing one-on-one discussion. + +## Having your code reviewed + +- The first reviewer of your code is _you_. Before you perform that first push + of your shiny new branch, read through the entire diff. Does it make sense? + Did you include something unrelated to the overall purpose of the changes? Did + you forget to remove any debugging code? +- Be grateful for the reviewer's suggestions. ("Good call. I'll make that + change.") +- Don't take it personally. The review is of the code, not of you. +- Explain why the code exists. ("It's like that because of these reasons. Would + it be more clear if I rename this class/file/method/variable?") +- Extract unrelated changes and refactorings into future merge requests/issues. +- Seek to understand the reviewer's perspective. +- Try to respond to every comment. +- Push commits based on earlier rounds of feedback as isolated commits to the + branch. Do not squash until the branch is ready to merge. Reviewers should be + able to read individual updates based on their earlier feedback. + +## Reviewing code + +Understand why the change is necessary (fixes a bug, improves the user +experience, refactors the existing code). Then: + +- Communicate which ideas you feel strongly about and those you don't. +- Identify ways to simplify the code while still solving the problem. +- Offer alternative implementations, but assume the author already considered + them. ("What do you think about using a custom validator here?") +- Seek to understand the author's perspective. +- If you don't understand a piece of code, _say so_. There's a good chance + someone else would be confused by it as well. +- After a round of line notes, it can be helpful to post a summary note such as + "LGTM :thumbsup:", or "Just a couple things to address." +- Avoid accepting a merge request before the build succeeds ("Merge when build + succeeds" is fine). + +## Credits + +Largely based on the [thoughtbot code review guide]. + +[thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md new file mode 100644 index 0000000000..c1cf2e77c2 --- /dev/null +++ b/doc/development/instrumentation.md @@ -0,0 +1,36 @@ +# Instrumenting Ruby Code + +GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby +code. This can be used to measure the time spent in a specific part of a larger +chunk of code. The resulting data is stored as a field in the transaction that +executed the block. + +To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure` +and give it a name: + +```ruby +Gitlab::Metrics.measure(:foo) do + ... +end +``` + +3 values are measured for a block: + +1. The real time elapsed, stored in NAME_real_time. +2. The CPU time elapsed, stored in NAME_cpu_time. +3. The call count, stored in NAME_call_count. + +Both the real and CPU timings are measured in milliseconds. + +Multiple calls to the same block will result in the final values being the sum +of all individual values. Take this code for example: + +```ruby +3.times do + Gitlab::Metrics.measure(:sleep) do + sleep 1 + end +end +``` + +Here the final value of `sleep_real_time` will be `3`, _not_ `1`. diff --git a/doc/development/scss_styleguide.md b/doc/development/scss_styleguide.md index 6c48c25448..a79f4073cd 100644 --- a/doc/development/scss_styleguide.md +++ b/doc/development/scss_styleguide.md @@ -72,9 +72,9 @@ p { margin: 0; padding: 0; } ### Colors -HEX (hexadecimal) colors short-form should use shortform where possible, and -should use lower case letters to differenciate between letters and numbers, e. -g. `#E3E3E3` vs. `#e3e3e3`. +HEX (hexadecimal) colors should use shorthand where possible, and should use +lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3` +vs. `#e3e3e3`. ```scss // Bad @@ -160,6 +160,7 @@ is slightly more performant. ``` ### Selectors with a `js-` Prefix + Do not use any selector prefixed with `js-` for styling purposes. These selectors are intended for use only with JavaScript to allow for removal or renaming without breaking styling. @@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with Note that this won't fix every problem, but it should fix a majority. +### Ignoring issues + +If you want a line or set of lines to be ignored by the linter, you can use +`// scss-lint:disable RuleName` ([more info][disabling-linters]): + +```scss +// This lint rule is disabled because the class name comes from a gem. +// scss-lint:disable SelectorFormat +.ui_charcoal { + background-color: #333; +} +// scss-lint:enable SelectorFormat +``` + +Make sure a comment is added on the line above the `disable` rule, otherwise the +linter will throw a warning. `DisableLinterReason` is enabled to make sure the +style guide isn't being ignored, and to communicate to others why the style +guide is ignored in this instance. + [csscomb]: https://github.com/csscomb/csscomb.js [node]: https://github.com/nodejs/node [npm]: https://www.npmjs.com/ [scss-lint]: https://github.com/brigade/scss-lint [scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md +[disabling-linters]: https://github.com/brigade/scss-lint#disabling-linters-via-source diff --git a/doc/development/testing.md b/doc/development/testing.md new file mode 100644 index 0000000000..672e3fb464 --- /dev/null +++ b/doc/development/testing.md @@ -0,0 +1,136 @@ +# Testing Standards and Style Guidelines + +This guide outlines standards and best practices for automated testing of GitLab +CE and EE. + +It is meant to be an _extension_ of the [thoughtbot testing +styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If +this guide defines a rule that contradicts the thoughtbot guide, this guide +takes precedence. Some guidelines may be repeated verbatim to stress their +importance. + +## Factories + +GitLab uses [factory_girl] as a test fixture replacement. + +- Factory definitions live in `spec/factories/`, named using the pluralization + of their corresponding model (`User` factories are defined in `users.rb`). +- There should be only one top-level factory definition per file. +- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and + should) call `create(...)` instead of `FactoryGirl.create(...)`. +- Make use of [traits] to clean up definitions and usages. +- When defining a factory, don't define attributes that are not required for the + resulting record to pass validation. +- When instantiating from a factory, don't supply attributes that aren't + required by the test. +- Factories don't have to be limited to `ActiveRecord` objects. + [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). + +[factory_girl]: https://github.com/thoughtbot/factory_girl +[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits + +## JavaScript + +GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on +the command line via `bundle exec teaspoon`, or via a web browser at +`http://localhost:3000/teaspoon` when the Rails server is running. + +- JavaScript tests live in `spec/javascripts/`, matching the folder structure of + `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding + `spec/javascripts/behaviors/autosize_spec.js.coffee` file. +- Haml fixtures required for JavaScript tests live in + `spec/javascripts/fixtures`. They should contain the bare minimum amount of + markup necessary for the test. + + > **Warning:** Keep in mind that a Rails view may change and + invalidate your test, but everything will still pass because your fixture + doesn't reflect the latest view. + +- Keep in mind that in a CI environment, these tests are run in a headless + browser and you will not have access to certain APIs, such as + [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), + which will have to be stubbed. + +[Teaspoon]: https://github.com/modeset/teaspoon +[Jasmine]: https://github.com/jasmine/jasmine + +## RSpec + +### General Guidelines + +- Use a single, top-level `describe ClassName` block. +- Use `described_class` instead of repeating the class name being described. +- Use `.method` to describe class methods and `#method` to describe instance + methods. +- Use `context` to test branching logic. +- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). +- Prefer `not_to` to `to_not`. +- Try to match the ordering of tests to the ordering within the class. +- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines + to separate phases. + +[four-phase-test]: https://robots.thoughtbot.com/four-phase-test + +### `let` variables + +GitLab's RSpec suite has made extensive use of `let` variables to reduce +duplication. However, this sometimes [comes at the cost of clarity][lets-not], +so we need to set some guidelines for their use going forward: + +- `let` variables are preferable to instance variables. Local variables are + preferable to `let` variables. +- Use `let` to reduce duplication throughout an entire spec file. +- Don't use `let` to define variables used by a single test; define them as + local variables inside the test's `it` block. +- Don't define a `let` variable inside the top-level `describe` block that's + only used in a more deeply-nested `context` or `describe` block. Keep the + definition as close as possible to where it's used. +- Try to avoid overriding the definition of one `let` variable with another. +- Don't define a `let` variable that's only used by the definition of another. + Use a helper method instead. + +[lets-not]: https://robots.thoughtbot.com/lets-not + +### Test speed + +GitLab has a massive test suite that, without parallelization, can take more +than an hour to run. It's important that we make an effort to write tests that +are accurate and effective _as well as_ fast. + +Here are some things to keep in mind regarding test performance: + +- `double` and `spy` are faster than `FactoryGirl.build(...)` +- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. +- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, + `spy`, or `double` will do. Database persistence is slow! +- Use `create(:empty_project)` instead of `create(:project)` when you don't need + the underlying Git repository. Filesystem operations are slow! +- Don't mark a feature as requiring JavaScript (through `@javascript` in + Spinach or `js: true` in RSpec) unless it's _actually_ required for the test + to be valid. Headless browser testing is slow! + +### Features / Integration + +- Feature specs live in `spec/features/` and should be named + `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. +- Use only one `feature` block per feature spec file. +- Use scenario titles that describe the success and failure paths. +- Avoid scenario titles that add no information, such as "successfully." +- Avoid scenario titles that repeat the feature title. + +## Spinach (feature) tests + +GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) +for its feature/integration tests in September 2012. + +As of March 2016, we are [trying to avoid adding new Spinach +tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, +opting for [RSpec feature](#features-integration) specs. + +Adding new Spinach scenarios is acceptable _only if_ the new scenario requires +no more than one new `step` definition. If more than that is required, the +test should be re-implemented using RSpec instead. + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 2f01defc11..a3e260a5f8 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -1,9 +1,5 @@ # UI Guide for building GitLab -## Best practices for creating new pages in GitLab - -TODO: write some best practices when develop GitLab features. - ## GitLab UI development kit We created a page inside GitLab where you can check commonly used html and css elements. diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 4cfb840294..5a9a158287 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -1,36 +1,99 @@ # Reply by email -GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. +GitLab can be set up to allow users to comment on issues and merge requests by +replying to notification emails. -## Get a mailbox +## Requirement -Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. +Reply by email requires an IMAP-enabled email account. GitLab allows you to use +three strategies for this feature: +- using email sub-addressing +- using a dedicated email address +- using a catch-all mailbox -If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). +### Email sub-addressing -To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). +**If your provider or server supports email sub-addressing, we recommend using it.** + +[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is +a feature where any email to `user+some_arbitrary_tag@example.com` will end up +in the mailbox for `user@example.com`, and is supported by providers such as +Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix +mail server which you can run on-premises. + +### Dedicated email address + +This solution is really simple to set up: you just have to create an email +address dedicated to receive your users' replies to GitLab notifications. + +### Catch-all mailbox + +A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will +"catch all" the emails addressed to the domain that do not exist in the mail +server. + +## How it works? + +### 1. GitLab sends a notification email + +When GitLab sends a notification and Reply by email is enabled, the `Reply-To` +header is set to the address defined in your GitLab configuration, with the +`%{key}` placeholder (if present) replaced by a specific "reply key". In +addition, this "reply key" is also added to the `References` header. + +### 2. You reply to the notification email + +When you reply to the notification email, your email client will: + +- send the email to the `Reply-To` address it got from the notification email +- set the `In-Reply-To` header to the value of the `Message-ID` header from the + notification email +- set the `References` header to the value of the `Message-ID` plus the value of + the notification email's `References` header. + +### 3. GitLab receives your reply to the notification email + +When GitLab receives your reply, it will look for the "reply key" in the +following headers, in this order: + +1. the `To` header +1. the `References` header + +If it finds a reply key, it will be able to leave your reply as a comment on +the entity the notification was about (issue, merge request, commit...). + +For more details about the `Message-ID`, `In-Reply-To`, and `References headers`, +please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4). ## Set it up +If you want to use Gmail / Google Apps with Reply by email, make sure you have +[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) +and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255). + +To set up a basic Postfix mail server with IMAP access on Ubuntu, follow +[these instructions](./postfix.md). + ### Omnibus package installations -1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the + feature and fill in the details for your specific IMAP server and email account: ```ruby # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com gitlab_rails['incoming_email_enabled'] = true - - # The email address including a placeholder for the key that references the item being replied to. - # The `%{key}` placeholder is added after the user part, before the `@`. + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" - + # Email account username # With third party providers, this is usually the full email address. # With self-hosted email servers, this is usually the user part of the email address. gitlab_rails['incoming_email_email'] = "incoming" # Email account password gitlab_rails['incoming_email_password'] = "[REDACTED]" - + # IMAP server host gitlab_rails['incoming_email_host'] = "gitlab.example.com" # IMAP server port @@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ```ruby # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com gitlab_rails['incoming_email_enabled'] = true - + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" - + # Email account username # With third party providers, this is usually the full email address. # With self-hosted email servers, this is usually the user part of the email address. gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account password gitlab_rails['incoming_email_password'] = "[REDACTED]" - + # IMAP server host gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server port @@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these gitlab_rails['incoming_email_mailbox_name'] = "inbox" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. - 1. Reconfigure GitLab and restart mailroom for the changes to take effect: ```sh @@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these cd /home/git/gitlab ``` -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature + and fill in the details for your specific IMAP server and email account: ```sh sudo editor config/gitlab.yml @@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "incoming+%{key}@gitlab.example.com" # Email account username @@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "gitlab-incoming+%{key}@gmail.com" # Email account username @@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these mailbox: "inbox" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. - 1. Enable `mail_room` in the init script at `/etc/default/gitlab`: ```sh @@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these incoming_email: enabled: true - # The email address including a placeholder for the key that references the item being replied to. - # The `%{key}` placeholder is added after the user part, before the `@`. + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "gitlab-incoming+%{key}@gmail.com" # Email account username diff --git a/doc/install/installation.md b/doc/install/installation.md index bffbc77650..f8f7d6a9eb 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -227,9 +227,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab -**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -283,9 +283,13 @@ sudo usermod -aG redis git # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, used when editing via web editor + # Configure Git global settings for git user + # 'autocrlf' is needed for the web editor sudo -u git -H git config --global core.autocrlf input + # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed + sudo -u git -H git config --global gc.auto 0 + # Configure Redis connection settings sudo -u git -H cp config/resque.yml.example config/resque.yml diff --git a/doc/integration/README.md b/doc/integration/README.md index 7c8f785a61..6fe04aa2a0 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,26 +19,15 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. +[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html + + ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. -You can find these within GitLab in the Services page under Project Settings if -you are at least a master on the project. -Project Services are a bit like plugins in that they allow a lot of freedom in -adding functionality to GitLab. For example there is also a service that can -send an email every time someone pushes new commits. -Because GitLab is open source we can ship with the code and tests for all -plugins. This allows the community to keep the plugins up to date so that they -always work in newer GitLab versions. - -For an overview of what projects services are available without logging in, -please see the [project_services directory][projects-code]. - -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [Project Service]: ../project_services/project_services.md -[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services ## SSL certificate errors diff --git a/doc/integration/github.md b/doc/integration/github.md index 886784a27c..1890edd7a4 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -17,7 +17,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL: 'https://gitlab.company.com/' + - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index cf1f98492e..30f0c15dac 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,228 +1,3 @@ # GitLab LDAP integration -GitLab can be configured to allow your users to sign with their LDAP credentials to integrate with e.g. Active Directory. - -The first time a user signs in with LDAP credentials, GitLab will create a new GitLab user associated with the LDAP Distinguished Name (DN) of the LDAP user. - -GitLab user attributes such as nickname and email will be copied from the LDAP user entry. - -## Security - -GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. -An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. - -We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. - -If a user is deleted from the LDAP server, they will be blocked in GitLab as well. -Users will be immediately blocked from logging in. However, there is an LDAP check -cache time of one hour. The means users that are already logged in or are using Git -over SSH will still be able to access GitLab for up to one hour. Manually block -the user in the GitLab Admin area to immediately block all access. - -## Configuring GitLab for LDAP integration - -To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. -In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server. - -Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration. -The old LDAP integration syntax still works in GitLab 7.4. -If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab. - -```ruby -# For omnibus packages -gitlab_rails['ldap_enabled'] = true -gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below -main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - - host: '_your_ldap_server' - port: 389 - uid: 'sAMAccountName' - method: 'plain' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' - - # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking - # a request if the LDAP server becomes unresponsive. - # A value of 0 means there is no timeout. - timeout: 10 - - # This setting specifies if LDAP server is Active Directory LDAP server. - # For non AD servers it skips the AD specific queries. - # If your LDAP server is not AD, set this to false. - active_directory: true - - # If allow_username_or_email_login is enabled, GitLab will ignore everything - # after the first '@' in the LDAP username submitted by the user on login. - # - # Example: - # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; - # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. - # - # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to - # disable this setting, because the userPrincipalName contains an '@'. - allow_username_or_email_login: false - - # To maintain tight control over the number of active users on your GitLab installation, - # enable this setting to keep new users blocked until they have been cleared by the admin - # (default: false). - block_auto_created_users: false - - # Base where we can search for users - # - # Ex. ou=People,dc=gitlab,dc=example - # - base: '' - - # Filter LDAP users - # - # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 - # Ex. (employeeType=developer) - # - # Note: GitLab does not support omniauth-ldap's custom filter syntax. - # - user_filter: '' - - # LDAP attributes that GitLab will use to create an account for the LDAP user. - # The specified attribute can either be the attribute name as a string (e.g. 'mail'), - # or an array of attribute names to try in order (e.g. ['mail', 'email']). - # Note that the user's LDAP login will always be the attribute specified as `uid` above. - attributes: - # The username will be used in paths for the user's own projects - # (like `gitlab.example.com/username/project`) and when mentioning - # them in issues, merge request and comments (like `@username`). - # If the attribute specified for `username` contains an email address, - # the GitLab username will be the part of the email address before the '@'. - username: ['uid', 'userid', 'sAMAccountName'] - email: ['mail', 'email', 'userPrincipalName'] - - # If no full name could be found at the attribute specified for `name`, - # the full name is determined using the attributes specified for - # `first_name` and `last_name`. - name: 'cn' - first_name: 'givenName' - last_name: 'sn' - -# GitLab EE only: add more LDAP servers -# Choose an ID made of a-z and 0-9 . This ID will be stored in the database -# so that GitLab can remember which LDAP server a user belongs to. -# uswest2: -# label: -# host: -# .... -EOS -``` - -If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. -Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. - -If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: - -``` -production: - # snip... - ldap: - enabled: false - servers: - main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - # snip... -``` - -## Enabling LDAP sign-in for existing GitLab users - -When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user. - -If the LDAP email attribute is not found in GitLab's database, a new user is created. - -In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials. - -GitLab recognizes the following LDAP attributes as email addresses: `mail`, `email` and `userPrincipalName`. - -If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `email: foo@example.com`, then the first attribute found wins -- in this case `foo@bar.com`. - -## Using an LDAP filter to limit access to your GitLab server - -If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter. -The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515). - -```ruby -# For omnibus packages; new LDAP server syntax -gitlab_rails['ldap_servers'] = YAML.load <<-EOS -main: - # snip... - user_filter: '(employeeType=developer)' -EOS -``` - -```yaml -# For installations from source; new LDAP server syntax -production: - ldap: - servers: - main: - # snip... - user_filter: '(employeeType=developer)' -``` - -Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: - -``` -(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) -``` - -Please note that GitLab does not support the custom filter syntax used by omniauth-ldap. - -## Limitations - -GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap) -which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation -of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP -server settings. - -### TLS Client Authentication -Not implemented by `Net::LDAP`. -So you should disable anonymous LDAP authentication and enable simple or SASL -authentication. TLS client authentication setting in your LDAP server cannot be -mandatory and clients cannot be authenticated with the TLS protocol. - -### TLS Server Authentication -Not supported by GitLab's configuration options. -When setting `method: ssl`, the underlying authentication method used by -`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with -the LDAP server before any LDAP-protocol data is exchanged but no validation of -the LDAP server's SSL certificate is performed. - -## Troubleshooting - -### Invalid credentials when logging in - -Make sure the user you are binding with has enough permissions to read the user's -tree and traverse it. - -Also make sure that the `user_filter` is not blocking otherwise valid users. - -To make sure that the LDAP settings are correct and GitLab can see your users, -execute the following command: - - -```bash -# For Omnibus installations -sudo gitlab-rake gitlab:ldap:check - -# For installations from source -sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production -``` - +This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 1c3dc707f6..8a7205caaa 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -131,8 +131,75 @@ On the sign in page there should now be a SAML button below the regular sign in Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. +## External Groups + +>**Note:** +This setting is only available on GitLab 8.7 and above. + +SAML login includes support for external groups. You can define in the SAML +settings which groups, to which your users belong in your IdP, you wish to be +marked as [external](../permissions/permissions.md). + +### Requirements + +First you need to tell GitLab where to look for group information. For this you +need to make sure that your IdP server sends a specific `AttributeStament` along +with the regular SAML response. Here is an example: + +```xml + + + SecurityGroup + Developers + Designers + + +``` + +The name of the attribute can be anything you like, but it must contain the groups +to which a user belongs. In order to tell GitLab where to find these groups, you need +to add a `groups_attribute:` element to your SAML settings. You will also need to +tell GitLab which groups are external via the `external_groups:` element: + +```yaml +{ name: 'saml', + label: 'Our SAML Provider', + groups_attribute: 'Groups', + external_groups: ['Freelancers', 'Interns'], + args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + } } +``` + ## Customization +### `auto_sign_in_with_provider` + +You can add this setting to your GitLab configuration to automatically redirect you +to your SAML server for authentication, thus removing the need to click a button +before actually signing in. + +For omnibus package: + +```ruby +gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml' +``` + +For installations from source: + +```yaml +omniauth: + auto_sign_in_with_provider: saml +``` + +Please keep in mind that every sign in attempt will be redirected to the SAML server, +so you will not be able to sign in using local credentials. Make sure that at least one +of the SAML users has admin permissions. + ### `attribute_statements` >**Note:** @@ -205,6 +272,10 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th where it can then be seen in the usual logs, or as a flash message in the login screen. +That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` +for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for +installations from source. + ### Invalid audience This error means that the IdP doesn't recognize GitLab as a valid sender and diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e6eb1cf381..4f199b6af6 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -31,7 +31,7 @@ _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ -For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. +GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). You can use GFM in @@ -47,10 +47,10 @@ You can also use other rich text files in GitLab. You might have to install a de GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or softreturns, are rendered if you end a line with two or more spaces - Roses are red [followed by two or more spaces] + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet @@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing ## URL auto-linking diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 3d375e47c8..6219693b8a 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -52,10 +52,11 @@ documentation](../workflow/add-user/add-user.md). | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | -| Force push to protected branches | | | | | | -| Remove protected branches | | | | | | +| Force push to protected branches [^2] | | | | | | +| Remove protected branches [^2] | | | | | | [^1]: If **Allow guest to access builds** is enabled in CI settings +[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner ## Group diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 3fea2cff0b..a5af620d9b 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,7 +1,24 @@ # Project Services Project services allow you to integrate GitLab with other applications. Below -is list of the currently supported ones. Click on the service links to see +is list of the currently supported ones. + +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. + +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. + +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. + +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +Click on the service links to see further configuration instructions and details. Contributions are welcome. ## Services diff --git a/doc/release/README.md b/doc/release/README.md deleted file mode 100644 index 52eca7c02a..0000000000 --- a/doc/release/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Release cycle - -Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/). - -## Release process documentation - -- [Monthly release](monthly.md), every month on the 22nd. -- [Patch release](patch.md), if there are serious regressions. -- [Security](security.md), for security problems. -- [Master](master.md), update process for the master branch. diff --git a/doc/release/howto_rc1.md b/doc/release/howto_rc1.md deleted file mode 100644 index 07c703142d..0000000000 --- a/doc/release/howto_rc1.md +++ /dev/null @@ -1,55 +0,0 @@ -# How to create RC1 - -The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub. - -### 1. Update the installation guide - -1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay) -1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) -1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794) -1. There might be other changes. Ask around. - -### 2. Create update guides - -[Follow this guide](howto_update_guides.md) to create update guides. - -### 3. Code quality indicators - -Make sure the code quality indicators are green / good. - -- [![Build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch) - -- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch) - -- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq) - -- [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available) - -- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq) - -### 4. Run release tool - -**Make sure EE `master` has latest changes from CE `master`** - -Get release tools - -``` -git clone git@dev.gitlab.org:gitlab/release-tools.git -cd release-tools -``` - -Release candidate creates stable branch from master. -So we need to sync master branch between all CE, EE and CI remotes. - -``` -bundle exec rake sync -``` - -Create release candidate and stable branch: - -``` -bundle exec rake release["x.x.0.rc1"] -``` - -Now developers can use master for merging new features. -So you should use stable branch for future code changes related to release. diff --git a/doc/release/howto_update_guides.md b/doc/release/howto_update_guides.md deleted file mode 100644 index 23d0959c33..0000000000 --- a/doc/release/howto_update_guides.md +++ /dev/null @@ -1,55 +0,0 @@ -# Create update guides - -1. Create: CE update guide from previous version. Like `7.3-to-7.4.md` -1. Create: CE to EE update guide in EE repository for latest version. -1. Update: `6.x-or-7.x-to-7.x.md` to latest version. -1. Create: CI update guide from previous version - -It's best to copy paste the previous guide and make changes where necessary. -The typical steps are listed below with any points you should specifically look at. - -#### 0. Any major changes? - -List any major changes here, so the user is aware of them before starting to upgrade. For instance: - -- Database updates -- Web server changes -- File structure changes - -#### 1. Stop server - -#### 2. Make backup - -#### 3. Do users need to update dependencies like `git`? - -- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release. - -- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release. - -#### 4. Get latest code - -#### 5. Does GitLab shell need to be updated? - -#### 6. Install libs, migrations, etc. - -#### 7. Any config files updated since last release? - -Check if any of these changed since last release: - -- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab) -- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl) -- -- [config/gitlab.yml.example](/config/gitlab.yml.example) -- [config/unicorn.rb.example](/config/unicorn.rb.example) -- [config/database.yml.mysql](/config/database.yml.mysql) -- [config/database.yml.postgresql](/config/database.yml.postgresql) -- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example) -- [config/resque.yml.example](/config/resque.yml.example) - -#### 8. Need to update init script? - -Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab) - -#### 9. Start application - -#### 10. Check application status diff --git a/doc/release/master.md b/doc/release/master.md deleted file mode 100644 index 9163e65200..0000000000 --- a/doc/release/master.md +++ /dev/null @@ -1,62 +0,0 @@ -# How to push GitLab CE master branch to all remotes. - -The source code of GitLab is available on multiple servers (with GitLab.com as the canonical source). -Synchronization between the repo's is done by the lead developer if there is no rush. -This happens a few times per workday on average. -If somebody else with access to all repo's wants to do it the instructions are below. -This is just to distribute changes, not to make them. - -## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c) - -```bash -gpa () -{ - git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master} -} -``` - -## Then add remotes to your local repo - -```bash -cd my-gitlab-ce-repo - -git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git -git remote add gh git@github.com:gitlabhq/gitlabhq.git -git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git -``` - -## Push to all remotes - -```bash -gpa -``` - -# Yanking packages from packages.gitlab.com - -In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the -procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg). - -You need to have: - -1. `package_cloud` gem installed (sudo gem install package_cloud) -1. Email and password for packages.gitlab.com -1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io) - -Example of yanking a package: - -```bash -package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm -``` - -If you are attempting this for the first time the output will look something like: - -```bash -Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one. -Email: -marin@gitlab.com -Password: - -Got your token. Writing a config file to /Users/marin/.packagecloud... success! -success! -Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done! -``` diff --git a/doc/release/monthly.md b/doc/release/monthly.md deleted file mode 100644 index 907c19e65a..0000000000 --- a/doc/release/monthly.md +++ /dev/null @@ -1,245 +0,0 @@ -# Monthly Release - -NOTE: This is a guide used by the GitLab the company to release GitLab. -As an end user you do not need to use this guide. - -The process starts 7 working days before the release. -The release manager doesn't have to perform all the work but must ensure someone is assigned. -The current release manager must schedule the appointment of the next release manager. -The new release manager should create overall issue to track the progress. -The release manager should be the only person pushing/merging commits to the x-y-stable branches. - -## Release Manager - -A release manager is selected that coordinates all releases the coming month, -including the patch releases for previous releases. -The release manager has to make sure all the steps below are done and delegated where necessary. -This person should also make sure this document is kept up to date and issues are created and updated. - -## Take vacations into account - -The time is measured in weekdays to compensate for weekends. -Do everything on time to prevent problems due to rush jobs or too little testing time. -Make sure that you take into account any vacations of maintainers. -If the release is falling behind immediately warn the team. - -## Create an overall issue and follow it - -Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with -the `release` label for easier searching. Replace the dates with actual dates -based on the number of workdays before the release. All steps from issue -template are explained below: - -``` -### Xth: (7 working days before the 22nd) - -- [ ] Triage the [Omnibus milestone] - -### Xth: (6 working days before the 22nd) - -- [ ] Determine QA person and notify this person -- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary -- [ ] Merge CE `master` into EE `master` via merge request (#LINK) -- [ ] Create CE and EE RC1 versions (#LINK) -- [ ] Build RC1 packages - -### Xth: (5 working days before the 22nd) - -- [ ] Do QA and fix anything coming out of it (#LINK) -- [ ] Close the [Omnibus milestone] -- [ ] Prepare the [blog post] - -### Xth: (4 working days before the 22nd) - -- [ ] Update GitLab.com with RC1 -- [ ] Create the regression issue in the CE issue tracker: - - ``` - This is a meta issue to index possible regressions in this monthly release - and any patch versions. - - Please do not raise or discuss issues directly in this issue but link to - issues that might warrant a patch release. If there is a Merge Request - that fixes the issue, please link to that as well. - - Please only post one regression issue and/or merge request per comment. - Comments will be updated by the release manager as they are addressed. - ``` - -- [ ] Tweet about RC1 release: - - ``` - GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable - Use at your own risk. Please link regressions issues from - LINK_TO_REGRESSION_ISSUE - ``` - -### Xth: (3 working days before the 22nd) - -- [ ] Merge `x-y-stable` into `x-y-stable-ee` -- [ ] Check that everyone is mentioned on the [blog post] using `@all` - -### Xth: (2 working days before the 22nd) - -- [ ] Check that MVP is added to the [MVP page] - -### Xth: (1 working day before the 22nd) - -- [ ] Merge `x-y-stable` into `x-y-stable-ee` -- [ ] Create CE and EE release candidates -- [ ] Create Omnibus tags and build packages for the latest release candidates -- [ ] Update GitLab.com with the latest RC - -### 22nd before 1200 CET: - -Release before 1200 CET / 2AM PST, to make sure the majority of our users -get the new version on the 22nd and there is sufficient time in the European -workday to quickly fix any issues. - -- [ ] Merge `x-y-stable` into `x-y-stable-ee` -- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) -- [ ] Create the 'x.y.0' version on version.gitlab.com -- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages) -- [ ] Try to do before 1200 CET: Publish the release [blog post] -- [ ] Tweet about the release -- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST - -[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE -[blog post]: LINK_TO_WIP_BLOG_POST -[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html -``` - -- - - - -## Update changelog - -Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is -asked if there is anything missing. - -There are three changelogs that need to be updated: CE, EE and CI. - -## Create RC1 (CE, EE, CI) - -[Follow this How-to guide](howto_rc1.md) to create RC1. - -## Prepare CHANGELOG for next release - -Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre. - -On creating the stable branches, notify the core team and developers. - -## QA - -Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. - -Use the omnibus packages created for RC1 of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). - -**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. - -#### Fix anything coming out of the QA - -Create an issue with description of a problem, if it is quick fix fix it yourself otherwise contact the team for advice. - -**NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted, -create an issue about it in order to discuss the next steps after the release. - -## Update GitLab.com with RC1 - -Use the omnibus EE packages created for RC1. -If there are big database migrations consider testing them with the production db on a VM. -Try to deploy in the morning. -It is important to do this as soon as possible, so we can catch any errors before we release the full version. - -## Create a regressions issue - -On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text: - -This is a meta issue to discuss possible regressions in this monthly release and any patch versions. -Please do not raise issues directly in this issue but link to issues that might warrant a patch release. -The decision to create a patch release or not is with the release manager who is assigned to this issue. -The release manager will comment here about the plans for patch releases. - -Assign the issue to the release manager and at mention all members of GitLab core team. If there are any known bugs in the release add them immediately. - -## Tweet about RC1 - -Tweet about the RC release: - -> GitLab x.x.0.rc1 is out. This release candidate is only suitable for testing. Please link regressions issues from LINK_TO_REGRESSION_ISSUE - -## Prepare the blog post - -1. The blog post template for this release should already exist and might have comments that were added during the month. -1. Fill out as much of the blog post template as you can. -1. Make sure the blog post contains information about the GitLab CI release. -1. Check the changelog of CE and EE for important changes. -1. Also check the CI changelog -1. Add a proposed tweet text to the blog post WIP MR description. -1. Create a WIP MR for the blog post -1. Make sure merge request title starts with `WIP` so it can not be accidentally merged until ready. -1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. -1. Decide with core team who will be the MVP user. -1. Create WIP MR for adding MVP to MVP page on website -1. Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. -1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) -1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor) -1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)' -1. Create a new merge request with complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the next release using the branch name `release-x-x-x`. - -## Create CE, EE, CI stable versions - -Get release tools - -``` -git clone git@dev.gitlab.org:gitlab/release-tools.git -cd release-tools -``` - -Bump version, create release tag and push to remotes: - -``` -bundle exec rake release["x.x.0"] -``` - -This will create correct version and tag and push to all CE, EE and CI remotes. - -Update [installation.md](/doc/install/installation.md) to the newest version in master. - - -## Create Omnibus tags and build packages - -Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md). -This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase. - -## Update GitLab.com with the stable version - -- Deploy the package (should not need downtime because of the small difference with RC1) -- Deploy the package for gitlab.com/ci - -## Release CE, EE and CI - -__1. Publish packages for new release__ - -Update `downloads/index.html` and `downloads/archive/index.html` in `www-gitlab-com` repository. - -__2. Publish blog for new release__ - -Doublecheck the everyone has been mentioned in the blog post. -Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository. - -__3. Tweet to blog__ - -Send out a tweet to share the good news with the world. -List the most important features and link to the blog post. - -Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE <link-to-blog-post> #gitlab" - -Consider creating a post on Hacker News. - -## Release new AMIs - -[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) - -## Create a WIP blogpost for the next release - -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). diff --git a/doc/release/patch.md b/doc/release/patch.md deleted file mode 100644 index 1c92143915..0000000000 --- a/doc/release/patch.md +++ /dev/null @@ -1,81 +0,0 @@ -# Things to do when doing a patch release - -NOTE: This is a guide for GitLab developers. If you are trying to install GitLab -see the latest stable [installation guide](install/installation.md) and if you -are trying to upgrade, see the [upgrade guides](update). - -## When to do a patch release - -Patch releases are done as-needed in order to fix regressions in the current -major release that cannot or should not wait until the next major release. -What's included and when to release is at the discretion of the release manager. - -## Release Procedure - -### Create a patch issue - -Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with -the `release` label, and assign it to the milestone of the corresponding major -release. - -Use the following template: - -``` -- Picked into respective `stable` branches: -- [ ] Merge `x-y-stable` into `x-y-stable-ee` -- [ ] release-tools: `x.y.z` -- omnibus-gitlab - - [ ] `x.y.z+ee.0` - - [ ] `x.y.z+ce.0` -- [ ] Deploy -- [ ] Add patch notice to [x.y regressions]() -- [ ] [Blog post]() -- [ ] [Tweet]() -- [ ] Add entry to version.gitlab.com -``` - -Update the issue with links to merge requests that need to be/have been picked -into the `stable` branches. - -### Preparation - -1. Verify that the issue can be reproduced -1. Note in the 'GitLab X.X regressions' that you will create a patch -1. Fix the issue on a feature branch, do this on the private GitLab development server -1. If it is a security issue, then assign it to the release manager and apply a 'security' label -1. Consider creating and testing workarounds -1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch -1. Make sure that the build has passed and all tests are passing -1. In a separate commit in the master branch update the CHANGELOG -1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X" -1. Merge CE stable branch into EE stable branch - -### Bump version - -Get release tools - -``` -git clone git@dev.gitlab.org:gitlab/release-tools.git -cd release-tools -``` - -Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now, -it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1. - -Create release tag and push to remotes: - -``` -bundle exec rake release["x.x.x"] -``` - -## Release - -1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) -1. Apply the patch to GitLab.com and the private GitLab development server -1. Apply the patch to ci.gitLab.com and the private GitLab CI development server -1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) -1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post -1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) -1. Create the 'x.y.0' version on version.gitlab.com -1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) -1. Create a new patch release issue for the next potential release diff --git a/doc/release/security.md b/doc/release/security.md deleted file mode 100644 index 118c016ba4..0000000000 --- a/doc/release/security.md +++ /dev/null @@ -1,76 +0,0 @@ -# Things to do when doing an out-of-bound security release - -NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update). - -## When to do a security release - -Do a security release when there is a critical issue that needs to be addresses before the next monthly release. Otherwise include it in the monthly release and note there was a security fix in the release announcement. - -## Security vulnerability disclosure - -Please report suspected security vulnerabilities in private to , also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. - -## Release Procedure - -1. Verify that the issue can be reproduced -1. Acknowledge the issue to the researcher that disclosed it -1. Inform the release manager that there needs to be a security release -1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server" -1. The MR with the security fix should get a 'security' label and be assigned to the release manager -1. Build the package for GitLab.com and do a deploy -1. Build the package for ci.gitLab.com and do a deploy -1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) -1. Create feature branches for the blog post on GitLab.com and link them from the code branch -1. Merge and publish the blog posts -1. Send tweets about the release from `@gitlabhq` -1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq) -1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of. -1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/) -1. Thank the security researcher in an email for their cooperation -1. Update the blog post and the CHANGELOG when we receive the CVE number - -The timing of the code merge into master should be coordinated in advance. - -After the merge we strive to publish the announcements within 60 minutes. - -## Blog post template - -XXX Security Advisory for GitLab - -A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited. - -### Version affected - -GitLab Community Edition XXX and lower - -GitLab Enterprise Edition XXX and lower - -### Fixed versions - -GitLab Community Edition XXX and up - -GitLab Enterprise Edition XXX and up - -### Impact - -On GitLab installations which use MySQL as their database backend it is possible for an attacker to assume the identity of any existing GitLab user in certain API calls. This attack can be performed by [unauthenticated|authenticated|XXX|PICKSOMETHING] users. - -### Workarounds - -If you are unable to upgrade you should apply the following patch and restart GitLab. - -XXX - -### Credit - -We want to thank XXX of XXX for the responsible disclosure of this vulnerability. - -## Email template - -We just announced a security advisory for GitLab at XXX - -Please contact us at support@gitlab.com if you have any questions. - -## Tweet template - -We just announced a security advisory for GitLab at XXX diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index 712e9fdf93..b9abcbd2c1 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -62,7 +62,26 @@ sudo -u git -H git checkout v0.7.1 sudo -u git -H make ``` -### 6. Install libs, migrations, etc. +### 6. Updates for PostgreSQL Users + +Starting with 8.6 users using GitLab in combination with PostgreSQL are required +to have the `pg_trgm` extension enabled for all GitLab databases. If you're +using GitLab's Omnibus packages there's nothing you'll need to do manually as +this extension is enabled automatically. Users who install GitLab without using +Omnibus (e.g. by building from source) have to enable this extension manually. +To enable this extension run the following SQL command as a PostgreSQL super +user for _every_ GitLab database: + +```sql +CREATE EXTENSION IF NOT EXISTS pg_trgm; +``` + +Certain operating systems might require the installation of extra packages for +this extension to be available. For example, users using Ubuntu will have to +install the `postgresql-contrib` package in order for this extension to be +available. + +### 7. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -84,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ``` -### 7. Update configuration files +### 8. Update configuration files #### New configuration options for `gitlab.yml` @@ -120,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -### 8. Updates for PostgreSQL Users - -Starting with 8.6 users using GitLab in combination with PostgreSQL are required -to have the `pg_trgm` extension enabled for all GitLab databases. If you're -using GitLab's Omnibus packages there's nothing you'll need to do manually as -this extension is enabled automatically. Users who install GitLab without using -Omnibus (e.g. by building from source) have to enable this extension manually. -To enable this extension run the following SQL command as a PostgreSQL super -user for _every_ GitLab database: - -```sql -CREATE EXTENSION IF NOT EXISTS pg_trgm; -``` - -Certain operating systems might require the installation of extra packages for -this extension to be available. For example, users using Ubuntu will have to -install the `postgresql-contrib` package in order for this extension to be -available. - ### 9. Start application sudo service gitlab start diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md new file mode 100644 index 0000000000..8599133a72 --- /dev/null +++ b/doc/update/8.6-to-8.7.md @@ -0,0 +1,154 @@ +# From 8.6 to 8.7 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-7-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-7-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all +sudo -u git -H git checkout v2.7.0 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index afdf1a682e..22e207b6d3 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook "path_with_namespace":"mike/diaspora", "default_branch":"master", "homepage":"http://example.com/mike/diaspora", - "url":"git@example.com:mike/diasporadiaspora.git", + "url":"git@example.com:mike/diaspora.git", "ssh_url":"git@example.com:mike/diaspora.git", "http_url":"http://example.com/mike/diaspora.git" }, "repository":{ "name": "Diaspora", - "url": "git@example.com:mike/diasporadiaspora.git", + "url": "git@example.com:mike/diaspora.git", "description": "", "homepage": "http://example.com/mike/diaspora", "git_http_url":"http://example.com/mike/diaspora.git", @@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository. X-Gitlab-Event: Tag Push Hook ``` - **Request body:** ```json @@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook "http_url":"http://example.com/jsmith/example.git" }, "repository":{ - "name": "jsmith", + "name": "Example", "url": "ssh://git@example.com/jsmith/example.git", "description": "", "homepage": "http://example.com/jsmith/example", @@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook }, "repository":{ "name":"diaspora", - "url":"git@example.com:mike/diasporadiaspora.git", + "url":"git@example.com:mike/diaspora.git", "description":"", "homepage":"http://example.com/mike/diaspora" }, diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 36cb9da238..9dc1e9b47e 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`: ```ruby gitlab_rails['lfs_enabled'] = false + +# Optionally, change the storage path location. Defaults to +# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to +# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default. gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects" ``` diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index c3b3577c44..db73309804 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -6,6 +6,7 @@ Feature: Dashboard And project "Shop" has push event And project "Shop" has CI enabled And project "Shop" has CI build + And project "Shop" has labels: "bug", "feature", "enhancement" And I visit dashboard page Scenario: I should see projects list @@ -50,6 +51,13 @@ Feature: Dashboard And I visit dashboard issues page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Filtering Issues by label + Given project "Shop" has issue "Bugfix1" with label "feature" + When I visit dashboard issues page + And I filter the list by label "feature" + Then I should see "Bugfix1" in issues list + @javascript Scenario: Visiting Project's issues after sorting Given I visit dashboard issues page diff --git a/features/dashboard/todos.feature b/features/dashboard/todos.feature index 1e7b1b50d6..8677b45081 100644 --- a/features/dashboard/todos.feature +++ b/features/dashboard/todos.feature @@ -36,3 +36,8 @@ Feature: Dashboard Todos Scenario: I filter by action Given I filter by "Mentioned" Then I should not see todos related to "Assignments" in the list + + @javascript + Scenario: I click on a todo row + Given I click on the todo + Then I should be directed to the corresponding page diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index 10bd6fec80..67f1e117f7 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -4,6 +4,7 @@ Feature: Project Forked Merge Requests And I am a member of project "Shop" And I have a project forked off of "Shop" called "Forked Shop" + @javascript Scenario: I submit new unassigned merge request to a forked project Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 823658b4f2..ecda4ea824 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -70,6 +70,7 @@ Feature: Project Merge Requests When I click link "Reopen" Then I should see reopened merge request "Bug NS-04" + @javascript Scenario: I submit new unassigned merge request Given I click link "New Merge Request" And I submit new merge request "Wiki Feature" diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 5062e34884..b5980b3510 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see 1 project at group list' do expect(find('span.last_activity/span')).to have_content('1') end + + step 'I filter the list by label "feature"' do + page.within ".labels-filter" do + find('.dropdown').click + click_link "feature" + end + end + + step 'I should see "Bugfix1" in issues list' do + page.within "ul.content-list" do + expect(page).to have_content "Bugfix1" + end + end + + step 'project "Shop" has issue "Bugfix1" with label "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user) + issue.labels << project.labels.find_by(title: 'feature') + end end diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index f4a5686553..e21af72a77 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "All" link' do - find('.js-author-search').click - find('.dropdown-menu-user-full-name', match: :first).click - - find('.js-assignee-search').click - find('.dropdown-menu-user-full-name', match: :first).click + find(".js-author-search").click + find(".dropdown-menu-author li a", match: :first).click + find(".js-assignee-search").click + find(".dropdown-menu-assignee li a", match: :first).click end def should_see(issue) diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 963e4f2136..a6e574f12a 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -26,6 +26,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should see todos assigned to me' do + page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' } expect(page).to have_content 'To do 4' expect(page).to have_content 'Done 0' @@ -41,6 +42,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end + page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" @@ -88,6 +90,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps should_not_see_todo "John Doe assigned you issue ##{issue.iid}" end + step 'I click on the todo' do + find('.todo:nth-child(1)').click + end + + step 'I should be directed to the corresponding page' do + page.should have_css('.identifier', text: 'Merge Request !1') + end + def should_see_todo(position, title, body, pending = true) page.within(".todo:nth-child(#{position})") do expect(page).to have_content title diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 7e4425ff66..612bb8fd8b 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -34,10 +34,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I fill out a "Merge Request On Forked Project" merge request' do - select @forked_project.path_with_namespace, from: "merge_request_source_project_id" - select @project.path_with_namespace, from: "merge_request_target_project_id" - select "fix", from: "merge_request_source_branch" - select "master", from: "merge_request_target_branch" + first('.js-source-project').click + first('.dropdown-source-project a', text: @forked_project.path_with_namespace) + + first('.js-target-project').click + first('.dropdown-target-project a', text: @project.path_with_namespace) + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'fix').click click_button "Compare branches and continue" @@ -115,10 +119,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s - expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s - expect(find(:select, "merge_request_source_branch", {}).value).to eq "" - expect(find(:select, "merge_request_target_branch", {}).value).to eq "master" + expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s + expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s + expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil + expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master" click_button "Compare branches" end @@ -127,7 +131,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'the target repository should be the original repository' do - expect(page).to have_select("merge_request_target_project_id", selected: @project.path_with_namespace) + expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}" end step 'I click "Assign to" dropdown"' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 2ab8956867..0ca2d6257c 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -15,7 +15,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I delete all labels' do page.within '.labels' do - page.all('.btn-remove').each do |remove| + page.all('.remove-row').each do |remove| remove.click sleep 0.05 end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 91fe19dd47..f0af0d097f 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -93,8 +93,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I submit new merge request "Wiki Feature"' do - select "fix", from: "merge_request_source_branch" - select "feature", from: "merge_request_target_branch" + find('.js-source-branch').click + find('.dropdown-source-branch .dropdown-content a', text: 'fix').click + + find('.js-target-branch').click + first('.dropdown-target-branch .dropdown-content a', text: 'feature').click + click_button "Compare branches" fill_in "merge_request_title", with: "Wiki Feature" click_button "Submit merge request" @@ -326,7 +330,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -334,7 +338,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion by user "John Doe" has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{user_exists("John Doe").name} started a discussion" + page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -350,7 +354,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -358,7 +362,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content "One comment to rule them all" end end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 243469b8e7..e072505e5d 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -213,13 +213,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse file link' do - expect(page).to have_link 'Browse File »' - expect(page).not_to have_link 'Browse Files »' + expect(page).to have_link 'Browse File' + expect(page).not_to have_link 'Browse Files' end step 'I see Browse code link' do - expect(page).to have_link 'Browse Files »' - expect(page).not_to have_link 'Browse File »' + expect(page).to have_link 'Browse Files' expect(page).not_to have_link 'Browse Directory »' end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 223b7277b5..9f6aed1c5b 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -85,7 +85,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I have an existing Wiki page with images linked on page' do - wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![image](image.jpg)", :markdown, "first commit") + wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit") @wiki_page = wiki.find_page("pictures") end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 906b66a4a6..1448c3f44c 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -125,7 +125,7 @@ module SharedDiffNote step 'I should only see one diff form' do page.within(diff_file_selector) do - expect(page).to have_css("form.new_note", count: 1) + expect(page).to have_css("form.new-note", count: 1) end end @@ -155,13 +155,13 @@ module SharedDiffNote step 'I should see a discussion reply button' do page.within(diff_file_selector) do - expect(page).to have_button('Reply') + expect(page).to have_button('Reply...') end end step 'I should see a temporary diff comment form' do page.within(diff_file_selector) do - expect(page).to have_css(".js-temp-notes-holder form.new_note") + expect(page).to have_css(".js-temp-notes-holder form.new-note") end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index fb0462d6e0..a3c3887ab4 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,7 +2,7 @@ module SharedNote include Spinach::DSL step 'I delete a comment' do - page.within('.notes') do + page.within('.main-notes-list') do find('.note').hover find(".js-note-delete").click end @@ -128,7 +128,7 @@ module SharedNote end step 'I edit the last comment with a +1' do - page.within(".notes") do + page.within(".main-notes-list") do find(".note").hover find('.js-note-edit').click end diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json new file mode 100644 index 0000000000..18d6e93e0f --- /dev/null +++ b/fixtures/emojis/digests.json @@ -0,0 +1,8597 @@ +[ + { + "name": "100", + "unicode": "1F4AF", + "digest": "6d57c7cc93335f853e1a5670233f121bc94730dbd82b2b3c5c5a509e092ef0fd" + }, + { + "name": "1234", + "unicode": "1F522", + "digest": "727763fd9f18fd5df59e9f78e678ea4ec753e674d70f15d4e77c7802067d660b" + }, + { + "name": "8ball", + "unicode": "1F3B1", + "digest": "1aecf21951452ba24e921ec71b3d313b7ddc2e185b0339c9e0eebc85be4f031d" + }, + { + "name": "a", + "unicode": "1F170", + "digest": "2272113a5bcb7faf8db7c1bd35df576d32f2f7cbd881463934ad3382eb87c723" + }, + { + "name": "ab", + "unicode": "1F18E", + "digest": "6f8a237751fdc84db4121f408272d9a23258515449610e4c6c54f50f6e995627" + }, + { + "name": "abc", + "unicode": "1F524", + "digest": "652a2381a7b587d8a52d5178e2d7d6c8600b33d36160fa69677943da374105bc" + }, + { + "name": "abcd", + "unicode": "1F521", + "digest": "35ade4fd3d75294ebb72c24490aa32745604edc6cabe095b90634cd3ce78c07b" + }, + { + "name": "accept", + "unicode": "1F251", + "digest": "8212ed158cc447c92813273fc915e84d3d5c4c48d1b38e498c088bad27ab8145" + }, + { + "name": "aerial_tramway", + "unicode": "1F6A1", + "digest": "8039d7f67e6e5b211066cab6cf2142afc3aca5c830a357369362c9b484029563" + }, + { + "name": "airplane", + "unicode": "2708", + "digest": "18f4dfac323555d8cdabb79148874c0185ce98e1a08e69414d236b23e502a854" + }, + { + "name": "airplane_arriving", + "unicode": "1F6EC", + "digest": "9a1c81d97512e5d0e3acec40290d00f616ec182140909859e366a734b9f840bb" + }, + { + "name": "airplane_departure", + "unicode": "1F6EB", + "digest": "e3c5ff4038db998c1897cb237d0b865da0bc60331c758f204e45a979d5fab445" + }, + { + "name": "airplane_northeast", + "unicode": "1F6EA", + "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" + }, + { + "name": "airplane_small", + "unicode": "1F6E9", + "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" + }, + { + "name": "airplane_small_up", + "unicode": "1F6E8", + "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" + }, + { + "name": "airplane_up", + "unicode": "1F6E7", + "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" + }, + { + "name": "alarm_clock", + "unicode": "23F0", + "digest": "84ddd7b3b857c165410b7b44863e5354ca0f3591c3bfe56231f12c9f7531a96f" + }, + { + "name": "alembic", + "unicode": "2697", + "digest": "45698914a21683f06931d807af171bcb6984e5ebce66012bba71b467565bd69d" + }, + { + "name": "alien", + "unicode": "1F47D", + "digest": "94dbe4e90614c654145aba93610c43e3ab86df8ca07391bd4e56383f9329c008" + }, + { + "name": "ambulance", + "unicode": "1F691", + "digest": "82ef36bcd13c88a4b2397c918b8048adc6bf045ed2532ff568e0dfd1b1b29c3c" + }, + { + "name": "amphora", + "unicode": "1F3FA", + "digest": "d3758d88aa1fc3be01894102f57479d3a49790510d38ad3d06a2774962010608" + }, + { + "name": "anchor", + "unicode": "2693", + "digest": "27c6034f769d9f020362fc5b227b9279651cc940861e727d1f6ccd59af98f851" + }, + { + "name": "angel", + "unicode": "1F47C", + "digest": "c1b8ad2adc7686e7fbbe4ec357071e7228a5e0762e001bb589e2f97ff258d5c7" + }, + { + "name": "angel_tone1", + "unicode": "1F47C-1F3FB", + "digest": "90b701c43311b1096c4a012d9905a186f1a16829ea2707921a8418c28617d751" + }, + { + "name": "angel_tone2", + "unicode": "1F47C-1F3FC", + "digest": "d6bcaf1b76e25d486d4ab9b159cf727782d508543d1ae27c8d2c12d2f13d6eb0" + }, + { + "name": "angel_tone3", + "unicode": "1F47C-1F3FD", + "digest": "3069285e6218c8083cb0085aa10017bcdea033e321d97ba339a84892074b903a" + }, + { + "name": "angel_tone4", + "unicode": "1F47C-1F3FE", + "digest": "dbb87019752d9caa94ce086858c1e3225b62e221ad599f5106548fda2456fc2b" + }, + { + "name": "angel_tone5", + "unicode": "1F47C-1F3FF", + "digest": "f77703df97720c27a128b5f3c0948b9e04a6b6b81ea5306468154f9bf56225db" + }, + { + "name": "anger", + "unicode": "1F4A2", + "digest": "2253b7ff0894f247bc6f04d841a748c56d6c94684880c13df42387691ff20e75" + }, + { + "name": "anger_left", + "unicode": "1F5EE", + "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" + }, + { + "name": "anger_right", + "unicode": "1F5EF", + "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" + }, + { + "name": "angry", + "unicode": "1F620", + "digest": "c4188ba70df99d8ccef5706d711176725d3dd50d62f065a177d68d85c7828107" + }, + { + "name": "anguished", + "unicode": "1F627", + "digest": "9c2347308133ae50dc04da62042fff847f4c477b2956b8aa976f0413899e38bc" + }, + { + "name": "ant", + "unicode": "1F41C", + "digest": "d2af2ed1cfe15d649aa329d965764a1e8726941d833841781a5b66d7dd0b0921" + }, + { + "name": "apple", + "unicode": "1F34E", + "digest": "a9babee24f454934a5e1fb8d781cbce354dfd88e8a8e01f02e8b30071fd40460" + }, + { + "name": "aquarius", + "unicode": "2652", + "digest": "1a168c252678847d1f9ef450887489e3bdc207ecae4b6fb05e92295ff861ae2c" + }, + { + "name": "aries", + "unicode": "2648", + "digest": "bde262a8795e12f8b0ebb3f0f8c3a56104062fcee8d5d678cf4bb445a7daf698" + }, + { + "name": "arrow_backward", + "unicode": "25C0", + "digest": "ddae36d1febf5c246e51d599e2898a8aa30cd47f88b5bcb469e3ca9d22538b97" + }, + { + "name": "arrow_double_down", + "unicode": "23EC", + "digest": "906f42b5f788128ed90d2d162cf03e6e595a50ad05e0aa5f64e925637379d0cd" + }, + { + "name": "arrow_double_up", + "unicode": "23EB", + "digest": "2129a57402980de6fc6f59ad8354525c2dbcd66d1b78f4de091181ddc81e0693" + }, + { + "name": "arrow_down", + "unicode": "2B07", + "digest": "370e4f41565d5dab245c20e45c502505a56d26c2392283781b841eb3e905edb2" + }, + { + "name": "arrow_down_small", + "unicode": "1F53D", + "digest": "98a2b183f2daec425160bbfce1d2b940b8baa0d5032fdacfa9453e39bed5651b" + }, + { + "name": "arrow_forward", + "unicode": "25B6", + "digest": "348627b8e0f55cf1e9ab19c9de1d170371b2c4cb4dda9a2aa8e0c558db08b18a" + }, + { + "name": "arrow_heading_down", + "unicode": "2935", + "digest": "96c64953fc3134711247bef320f252c48993ebc90494925b7fee42ffce2a2ec2" + }, + { + "name": "arrow_heading_up", + "unicode": "2934", + "digest": "94f94e74176cc050703b3584f3f700debf86e4e61b893a441825a21fa3f8ce74" + }, + { + "name": "arrow_left", + "unicode": "2B05", + "digest": "4553be62a63d7550deac4f7dbeffce6006f769ae6cddfb8c795671672011ba0b" + }, + { + "name": "arrow_lower_left", + "unicode": "2199", + "digest": "10f83c252110d705cdcfebc35a70c341ad288730d0c0729479e3a96e263d5120" + }, + { + "name": "arrow_lower_right", + "unicode": "2198", + "digest": "ee33abd4c96c19e9b80a2fc1500ba8ecaa6668c49310cc816a496e8c61af3850" + }, + { + "name": "arrow_right", + "unicode": "27A1", + "digest": "2611e9138a2651916f414015d0287f5f0af266514d96a42915d32b04fb652a90" + }, + { + "name": "arrow_right_hook", + "unicode": "21AA", + "digest": "628b06384a2963a4fe81e9fbf4e22511f697878d9b9db7d2fc98f8aadbe8f4f9" + }, + { + "name": "arrow_up", + "unicode": "2B06", + "digest": "c09e5f41c01028b45707c525d30d3d6731ec57b7447f0d7ba4ad6c1404449e5c" + }, + { + "name": "arrow_up_down", + "unicode": "2195", + "digest": "e7fd92d24a01702f76c7fcc0de998bc81fbfb93711d076984f6da91d1dccd84c" + }, + { + "name": "arrow_up_small", + "unicode": "1F53C", + "digest": "bc48dad74bc1d0c5579cbf5e3d005314b0d21bc5b5ebbba2b05136e33f49296d" + }, + { + "name": "arrow_upper_left", + "unicode": "2196", + "digest": "792a9709f03843024e53d201cb4769c59b656c3bf0dff2306e8e605493a66b93" + }, + { + "name": "arrow_upper_right", + "unicode": "2197", + "digest": "ee934b0c9cff270efd30a6cafc15253d405efd2c93b4785ac2ed4ea6420266a6" + }, + { + "name": "arrows_clockwise", + "unicode": "1F503", + "digest": "914f4120513730d7a19c9f8c4e59223a90568de0b25a225b712b31fa9697ef4f" + }, + { + "name": "arrows_counterclockwise", + "unicode": "1F504", + "digest": "86d87597e4e3db6dbba9907ee82412db0cbab1ea875bd0be6505dd886dc19b90" + }, + { + "name": "art", + "unicode": "1F3A8", + "digest": "dfc6b0da780199df86507d65b0499ba1706c266ae7badcb0e7fb5b85af7c9578" + }, + { + "name": "articulated_lorry", + "unicode": "1F69B", + "digest": "4c4de240ebd175f7b53453eda4e51f2e57d0db2a98d317f804116e14e47cff1d" + }, + { + "name": "ascending_notes", + "unicode": "1F39C", + "digest": "33432042771d456338dda5d98e49322d3600f2cc9049963480c7c38d9de1ef0a" + }, + { + "name": "asterisk", + "unicode": "002A-20E3", + "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" + }, + { + "name": "astonished", + "unicode": "1F632", + "digest": "58632b97e274ade5183752db2b3c5c4fe29effcd5a9720a8d01fa809b97023dc" + }, + { + "name": "athletic_shoe", + "unicode": "1F45F", + "digest": "1fc55d85a4d6751f9e60467801b051d2fb3341bdcc33b8d3695d5143359edb43" + }, + { + "name": "atm", + "unicode": "1F3E7", + "digest": "bf827ef6c349f5b6912d821457975a4720d1750529d907e94ece429b7a388d7e" + }, + { + "name": "atom", + "unicode": "269B", + "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" + }, + { + "name": "b", + "unicode": "1F171", + "digest": "9116256b3189977e37f6da7ddedf82bb29b0358829a4e8718fd59e51d9b86b3c" + }, + { + "name": "baby", + "unicode": "1F476", + "digest": "66596bea11015154e0b1752b85f349f4286c6643ee6f51ee5e60e0d625c4ae9a" + }, + { + "name": "baby_bottle", + "unicode": "1F37C", + "digest": "ed42994b4a539b8bfeccde0f3c7e9c7f54d6696ff48ce7e48171bbab51002348" + }, + { + "name": "baby_chick", + "unicode": "1F424", + "digest": "ea2cfa0e5c2cbff5fffdb52cc04dfe7872834bd7cfeaa45e0541b8faffcbd0e9" + }, + { + "name": "baby_symbol", + "unicode": "1F6BC", + "digest": "65df04dff8739b86f7663ae9c0648927341f360a986655e109721b0e16013b75" + }, + { + "name": "baby_tone1", + "unicode": "1F476-1F3FB", + "digest": "bc747527a2d723cf99ef3fc2539c19d29634c92ff417736982d3bf87d65d06eb" + }, + { + "name": "baby_tone2", + "unicode": "1F476-1F3FC", + "digest": "b82bba7a666b7d070751726e54acc7fb8f96e2dfc09e9610d61cfd20947aef9c" + }, + { + "name": "baby_tone3", + "unicode": "1F476-1F3FD", + "digest": "7f45dfd4ea2ae8515d419ffa13e7ee5c625b024b4e521ace5344c414bb929da0" + }, + { + "name": "baby_tone4", + "unicode": "1F476-1F3FE", + "digest": "80b1854626616f15426649cc6415e4911a55c8f761422fe48a08af9e8ac6a7cb" + }, + { + "name": "baby_tone5", + "unicode": "1F476-1F3FF", + "digest": "9f890804d19a61bee76a29644c818045dd96cf69d67cfbca2d11f4ad376b27da" + }, + { + "name": "back", + "unicode": "1F519", + "digest": "1dc73947b8f56e033777ca3f747407923bd16b07e53a6c78b09950ca474b7e7a" + }, + { + "name": "badminton", + "unicode": "1F3F8", + "digest": "3f95180c1175d0248ebf4b8650cf86566c39e0486d828078244080194c14d4fe" + }, + { + "name": "baggage_claim", + "unicode": "1F6C4", + "digest": "7c1a69511aa2a93984d601da4d1cef1cb4cefbbf127b1486278da8c01345bbf3" + }, + { + "name": "balloon", + "unicode": "1F388", + "digest": "a10c2b0865179cdbdef339494ec9b2a109451a356e53738d6a9dd43232500956" + }, + { + "name": "ballot_box", + "unicode": "1F5F3", + "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" + }, + { + "name": "ballot_box_check", + "unicode": "1F5F9", + "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" + }, + { + "name": "ballot_box_with_check", + "unicode": "2611", + "digest": "5f5cec7fe462557d31e8d2b836534c1e76d546cc0061236fa2af3667972b84aa" + }, + { + "name": "ballot_box_x", + "unicode": "1F5F5", + "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" + }, + { + "name": "ballot_x", + "unicode": "1F5F4", + "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" + }, + { + "name": "bamboo", + "unicode": "1F38D", + "digest": "feb0cf2f1012a1c0649b8c66f7e96e2d8bcdefe879c5a52dab3e25c51009e3b2" + }, + { + "name": "banana", + "unicode": "1F34C", + "digest": "aa9a1e6db00efa94a7f414c570eff7fc29011be64031a24d03b7f37b617cfd2d" + }, + { + "name": "bangbang", + "unicode": "203C", + "digest": "bdd350766ccd1c0138f6294f7ebfa3e9867b02bda40a743f7062e52c68358765" + }, + { + "name": "bank", + "unicode": "1F3E6", + "digest": "c9648c93049cf8e7884242e58ae3145383d2e5034c9090e0d34c53f5bbce397f" + }, + { + "name": "bar_chart", + "unicode": "1F4CA", + "digest": "942277f72a5b754b13454dab62c85b1ff3447544f38ec76a285f3be32f6f5d12" + }, + { + "name": "barber", + "unicode": "1F488", + "digest": "e1526eea685aafc56fb83d07f8ff63c9967600e447b0e5f831a17d6153f2062d" + }, + { + "name": "baseball", + "unicode": "26BE", + "digest": "3d028b16a898f3a15874bc9d3891f9fbf59ea1c226c5c774eddb58a712c489ae" + }, + { + "name": "basketball", + "unicode": "1F3C0", + "digest": "b2f5a3904d505db066337a24fc840ef75b49ef4c5f152227d8e632ff82285b12" + }, + { + "name": "basketball_player", + "unicode": "26F9", + "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" + }, + { + "name": "basketball_player_tone1", + "unicode": "26F9-1F3FB", + "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" + }, + { + "name": "basketball_player_tone2", + "unicode": "26F9-1F3FC", + "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" + }, + { + "name": "basketball_player_tone3", + "unicode": "26F9-1F3FD", + "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" + }, + { + "name": "basketball_player_tone4", + "unicode": "26F9-1F3FE", + "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" + }, + { + "name": "basketball_player_tone5", + "unicode": "26F9-1F3FF", + "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" + }, + { + "name": "bath", + "unicode": "1F6C0", + "digest": "ae6301a6354630cd9dc06a5137f23f826d019c8298b2b012b6ff31b773a910b6" + }, + { + "name": "bath_tone1", + "unicode": "1F6C0-1F3FB", + "digest": "fce7ae2e7ef3f7f44f36c2ad49348b4cf7fce0b0c17e1a90a1e85734cee95b2a" + }, + { + "name": "bath_tone2", + "unicode": "1F6C0-1F3FC", + "digest": "4d1c9444f16467488fe939fdad279d6855d28be564e5dcc1990451c4b9ae8c95" + }, + { + "name": "bath_tone3", + "unicode": "1F6C0-1F3FD", + "digest": "9a59a4360effb48af4cbb1a953655ef61e69375407038b4d0bd8068fbaf3cc16" + }, + { + "name": "bath_tone4", + "unicode": "1F6C0-1F3FE", + "digest": "01aafa8a53a08018b9fbf28ec6b3b918d6bd0dee7a891196f32f81f60d114f0e" + }, + { + "name": "bath_tone5", + "unicode": "1F6C0-1F3FF", + "digest": "2733e81ccaee21231c2e47e3310b431e9bd784bf34f0db609f8eadcee359500d" + }, + { + "name": "bathtub", + "unicode": "1F6C1", + "digest": "9515e3bb9ab41350305e64fc6877aae82d51e1ba8ce8b2b4b8ffaeda960820cd" + }, + { + "name": "battery", + "unicode": "1F50B", + "digest": "7d4d475c1d5b1be55c319953e3363ff864fe4fcd921a8aa649b9a547c0894deb" + }, + { + "name": "beach", + "unicode": "1F3D6", + "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" + }, + { + "name": "beach_umbrella", + "unicode": "26F1", + "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" + }, + { + "name": "bear", + "unicode": "1F43B", + "digest": "b5ac126875c20c82b9e3140b143233944a2e4132d781d0b575e83673988523cb" + }, + { + "name": "bed", + "unicode": "1F6CF", + "digest": "1919245d7a76799aad0533eb72db2cbaa1f32ee8231a0c1989d3f233f2d42370" + }, + { + "name": "bee", + "unicode": "1F41D", + "digest": "69ada63403c8dabae39c63ba143143aeb59b66faae6aa82d8342337925a9e6b5" + }, + { + "name": "beer", + "unicode": "1F37A", + "digest": "b71dd6efdb4ce7d9d71fdbf82a2ccf83841fb0cceb119ee7da1e575d3bfa853c" + }, + { + "name": "beers", + "unicode": "1F37B", + "digest": "994108cebfe0c614c05967af4e3864d8adbbfcf7cccef1cbd42a47b7dfabf80c" + }, + { + "name": "beetle", + "unicode": "1F41E", + "digest": "ec351ce238a81711eef00e5be1de2e198423cf524b60e531d435902b44420edc" + }, + { + "name": "beginner", + "unicode": "1F530", + "digest": "13288d9fc221dc02f4181b998104e13c3c5c98d3c4e650186bef59a46d39f6f0" + }, + { + "name": "bell", + "unicode": "1F514", + "digest": "784b9a82814ce14a264e54b3a8f8e706f3c7b763646d9f8174c4aa84ad41ef09" + }, + { + "name": "bellhop", + "unicode": "1F6CE", + "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" + }, + { + "name": "bento", + "unicode": "1F371", + "digest": "d59314b17a8646d4a78fefb7b79f289f33d4aaea893fed4cad0b890df63395e7" + }, + { + "name": "bicyclist", + "unicode": "1F6B4", + "digest": "e7359d615d40325bb08a145cfebde2ecef448deeb21695a34b55d3ccb971447f" + }, + { + "name": "bicyclist_tone1", + "unicode": "1F6B4-1F3FB", + "digest": "e45808faa32f4ffb881d3569c0b8e2c69d4a64665f4d1fae24d7a1e5f1d3ea4b" + }, + { + "name": "bicyclist_tone2", + "unicode": "1F6B4-1F3FC", + "digest": "92a3494270d1da6a117e92402c7898d4a7fffbe3d6143fb9ae445c4827c0c8a4" + }, + { + "name": "bicyclist_tone3", + "unicode": "1F6B4-1F3FD", + "digest": "6fdf1db2bbd08d06b643b08f0f29daeaa20e0b8c8abec21132191f435cc05e42" + }, + { + "name": "bicyclist_tone4", + "unicode": "1F6B4-1F3FE", + "digest": "d9c27848e1bcc8197c858e1ef12a537f4ed6c77fb211b6731388dc88c2bb7a61" + }, + { + "name": "bicyclist_tone5", + "unicode": "1F6B4-1F3FF", + "digest": "4892af1a8a0229a813d7b8e3d88481c2365e3e1a5ce2e0e27ce432c5336da810" + }, + { + "name": "bike", + "unicode": "1F6B2", + "digest": "e726f97b5432f46ed51328c0930d1d63b3a2d7b67c5c2303a5ca997083cfcac1" + }, + { + "name": "bikini", + "unicode": "1F459", + "digest": "7612fcb72c005ae7172260825f588d6995f2bc919cb3d283dd4591f6872a1855" + }, + { + "name": "biohazard", + "unicode": "2623", + "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" + }, + { + "name": "bird", + "unicode": "1F426", + "digest": "3f219e5aa18e2f1febfd368ec133786cd2eab357db79984cb8ba07fed0eec7cd" + }, + { + "name": "birthday", + "unicode": "1F382", + "digest": "9eb1adb0170ab851042cb3da8b64f02f4e4b63e7a07db405b55b50f5bbd3cacf" + }, + { + "name": "black_circle", + "unicode": "26AB", + "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" + }, + { + "name": "black_joker", + "unicode": "1F0CF", + "digest": "1eb85b8e2b93dec221a97a1c309dee3683408f6166e1a1a1bd83cf2f64f007dd" + }, + { + "name": "black_large_square", + "unicode": "2B1B", + "digest": "0ff2112227c38ed8c30b0bddf2300e87d2a244cd7fe81886a1cb1a287a7e8bb6" + }, + { + "name": "black_medium_small_square", + "unicode": "25FE", + "digest": "f1010aa694084ad4655a9d4ce5a1711eaab21029e31bf8798253f0ad644e8abb" + }, + { + "name": "black_medium_square", + "unicode": "25FC", + "digest": "06bf48ffbc84e71bbb90aa0f6c3f9f53533c6fd063ff168cefdb0a050dcf8302" + }, + { + "name": "black_nib", + "unicode": "2712", + "digest": "c1361df4a5ae9f2ed121d26928021e96c6865331861e1960700d39cb1bd49355" + }, + { + "name": "black_small_square", + "unicode": "25AA", + "digest": "d430ec419869fa1b5ba980ddeecb4c5ad5050a2b3421e45048cc184a6fc46899" + }, + { + "name": "black_square_button", + "unicode": "1F532", + "digest": "85b6587b6b2c3544ddb7bc07207b0740e437744ba134835836153899ae396135" + }, + { + "name": "blossom", + "unicode": "1F33C", + "digest": "029bbe385e07e2017dd918d685e107678c9c0e919a3bd1521b7a0d7c9172da05" + }, + { + "name": "blowfish", + "unicode": "1F421", + "digest": "b5ee9f6ffabb74e3024067f016d17a631ee98536cb9c7269d55fa867f95a54fb" + }, + { + "name": "blue_book", + "unicode": "1F4D8", + "digest": "6fbf227fb9facc1957bb9dfb31749cbfe66c3afe8081347f2471fd64ef2e6b3a" + }, + { + "name": "blue_car", + "unicode": "1F699", + "digest": "e61ef2299d11fc01e9d6c496d188a7211633946706f6e771c412368346ca16f4" + }, + { + "name": "blue_heart", + "unicode": "1F499", + "digest": "1af8d04173e0a984360786f6031220000dd548b8c912a68fd51f2ba490a9e16a" + }, + { + "name": "blush", + "unicode": "1F60A", + "digest": "d615cda0f7c185ed8a92008204043ef769f3b7fb5424d595aeaaf3827bcdbd73" + }, + { + "name": "boar", + "unicode": "1F417", + "digest": "c23a06db0337597e361ae581eacd4faf9926c6b7db0510d3599eb2e2a73315cb" + }, + { + "name": "bomb", + "unicode": "1F4A3", + "digest": "0099e7435eba35f4f3ad273993293693a8b5cd110567c95ed83e5b4e2d0978ff" + }, + { + "name": "book", + "unicode": "1F4D6", + "digest": "152408f2ff9949b7cbe57f623e4f875aa8dd0b02317e03cc914e1ea3712b3fc7" + }, + { + "name": "book2", + "unicode": "1F56E", + "digest": "26d6b66a1957e7750b3e22eb2e46d0cc85932977bbb81d3d8482ec1ec58ee12b" + }, + { + "name": "bookmark", + "unicode": "1F516", + "digest": "a2e0c6f5466c1b2fc148b20f6afcf4a878f4df55b0181f61fffa3ff727dcb251" + }, + { + "name": "bookmark_tabs", + "unicode": "1F4D1", + "digest": "16135d62ff440722bd1ce8f84219be6a5eb3120a1597bfda4aeed4a2d9e7d7b2" + }, + { + "name": "books", + "unicode": "1F4DA", + "digest": "ba019e4174639440caec424b30dfa016fe71a6f7436fe63025a2e3609ebfc012" + }, + { + "name": "boom", + "unicode": "1F4A5", + "digest": "ec26246935c99749950612d69c06435ccdc126f14426a48a7599c5b6b91d9d58" + }, + { + "name": "boot", + "unicode": "1F462", + "digest": "7ed639d52e285b0f46064dd4e1f4a8fb5814e1b2dc47c6f93cb349a6ac7ea97a" + }, + { + "name": "bouquet", + "unicode": "1F490", + "digest": "b699f13af218560344f3571436f87b6f8c5c9f0fa0308836937667241b3fc7aa" + }, + { + "name": "bouquet2", + "unicode": "1F395", + "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" + }, + { + "name": "bow", + "unicode": "1F647", + "digest": "5e260c38cfc80cd2f20ef78d982126dbf90934f7afa12c96d0b7b413beb6d4e0" + }, + { + "name": "bow_and_arrow", + "unicode": "1F3F9", + "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" + }, + { + "name": "bow_tone1", + "unicode": "1F647-1F3FB", + "digest": "d3ec7ef70b355ba310d6fae7130a4e4cd11526b6e219474b5678a2b3ba1077f0" + }, + { + "name": "bow_tone2", + "unicode": "1F647-1F3FC", + "digest": "c2905c0feba15fbc533cc6b36038eeda30f729182aa544f1d9164f5ccfed64d5" + }, + { + "name": "bow_tone3", + "unicode": "1F647-1F3FD", + "digest": "298fc646d96c307eaa137c80b403d8355539ed8af13d3954a4ccacef67d341fa" + }, + { + "name": "bow_tone4", + "unicode": "1F647-1F3FE", + "digest": "27db8401aa62a2544b24ff839b332958b5e8c3ab3fd7a289d3c62c654705da60" + }, + { + "name": "bow_tone5", + "unicode": "1F647-1F3FF", + "digest": "168cdf834edb54723cf1c32311d4117c288132c5f76d6c415726c7484158c52a" + }, + { + "name": "bowling", + "unicode": "1F3B3", + "digest": "0e888bcd1a5cc1ea7b07cea255ccb04dcdc87b0337b74cdc96a708aad7975768" + }, + { + "name": "boy", + "unicode": "1F466", + "digest": "f349ab3e1015b4ccda5faab6a355f9c38e36e7c1cd667084563a14a2b11036ea" + }, + { + "name": "boy_tone1", + "unicode": "1F466-1F3FB", + "digest": "4d04a5e45c9f9749de580321a212e14304b4ffcd229fa971fb59d97e6124262f" + }, + { + "name": "boy_tone2", + "unicode": "1F466-1F3FC", + "digest": "0c9d6b6b1b3da68b9ef1f0f01efa4d170a48cfc66de4f577f8669c160b81cc97" + }, + { + "name": "boy_tone3", + "unicode": "1F466-1F3FD", + "digest": "7dbecace78edb2aceffce6cb4d49ca132b93d80c26a8f1526a18832a2f23454a" + }, + { + "name": "boy_tone4", + "unicode": "1F466-1F3FE", + "digest": "49f9c633afa8ff81068c78717e0012f8936fb3dcdb8b57342410f57f0635ae7c" + }, + { + "name": "boy_tone5", + "unicode": "1F466-1F3FF", + "digest": "17e2ec379c7b542e6c2c5deef992af5f1fbaa3e288d1f71c8c984fb91a698cd4" + }, + { + "name": "boys_symbol", + "unicode": "1F6C9", + "digest": "47fadbcb876ca436264ce2f3ebd1472bd68f55cc2b4833bf054335be9dc7a0f2" + }, + { + "name": "bread", + "unicode": "1F35E", + "digest": "43697495538bfed11ed75213af8b1bdc14ef359d9b472cd7f9130fcb0a198680" + }, + { + "name": "bride_with_veil", + "unicode": "1F470", + "digest": "37e75fbb2b0d06c900d51269b99107c60b61453dbf218b54df3011a455cd6dc3" + }, + { + "name": "bride_with_veil_tone1", + "unicode": "1F470-1F3FB", + "digest": "44072e54e0618d2675a5bfd6572108590e51e8e733381e091e8754ee96c2cf20" + }, + { + "name": "bride_with_veil_tone2", + "unicode": "1F470-1F3FC", + "digest": "f0acd961e108db9d9dd5d1b06e708b2eb6a7ef7235d6c8678b9319077faf4fa8" + }, + { + "name": "bride_with_veil_tone3", + "unicode": "1F470-1F3FD", + "digest": "3f7adddb41ead3cd07098799ab2a5b8e8842344307d9045264403fb685f20555" + }, + { + "name": "bride_with_veil_tone4", + "unicode": "1F470-1F3FE", + "digest": "5f7199fd99319651f3a7b3553cc5387c59b65cac1eb020441e19b5c12c807dc7" + }, + { + "name": "bride_with_veil_tone5", + "unicode": "1F470-1F3FF", + "digest": "4b1f6c33dd72a3a11c764bb00e7be7441b39c7af78aae52141276a279d63ab78" + }, + { + "name": "bridge_at_night", + "unicode": "1F309", + "digest": "f81cc36de8edbdf3fe4d55932d5c6c8ad429487ec1f7af044611b6dc950ee09c" + }, + { + "name": "briefcase", + "unicode": "1F4BC", + "digest": "a3c3e802191f3e131683dac1fcd81e294dea72af8e65c94972990924c79c5619" + }, + { + "name": "broken_heart", + "unicode": "1F494", + "digest": "4dee349274c2ea44d1c0395cbd39356b88897b0c45040aa40d8cb2607ee67420" + }, + { + "name": "bug", + "unicode": "1F41B", + "digest": "bac4660ee8dcbef0023691804ee3fad3ea3d4bac20d847a5913cee6e7dca826c" + }, + { + "name": "bulb", + "unicode": "1F4A1", + "digest": "af5394230f95781c7eb8054b1a13732a6e6170318599c79e9ca2a816a5b821a2" + }, + { + "name": "bullettrain_front", + "unicode": "1F685", + "digest": "59afcd289500bd4148b1b91f560a5ce8ac9e1b52eddb8fec857ff5d171f017fb" + }, + { + "name": "bullettrain_side", + "unicode": "1F684", + "digest": "79ff8f579081a2f1c3b05311a18ca432adb026a7860875cea4a5460e49b2a474" + }, + { + "name": "bullhorn", + "unicode": "1F56B", + "digest": "a4ca5cbfe299e8ccd148d17055d2d395cf8515e416bf771044c9a670509a8254" + }, + { + "name": "bullhorn_waves", + "unicode": "1F56C", + "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" + }, + { + "name": "burrito", + "unicode": "1F32F", + "digest": "4babb1af1136ab2334d26495b0be779d0bcc9516fd956fc07ffde427d11122f0" + }, + { + "name": "bus", + "unicode": "1F68C", + "digest": "476e7a5e92f64038e5012205395efead51f1c10b3edb25380f38da97e2412edd" + }, + { + "name": "busstop", + "unicode": "1F68F", + "digest": "3bcf82872ab6abb0278238c71bd004a40c46696bdda05f54c153d45d6fe88f15" + }, + { + "name": "bust_in_silhouette", + "unicode": "1F464", + "digest": "2230844993ab011fe2756a1aa3873ff7d5f7d888bddec408ba0b32e4f6003570" + }, + { + "name": "busts_in_silhouette", + "unicode": "1F465", + "digest": "d1c3cb6d437616834425a53621c0bc0a6b368d745dd9da2300a3db4543d57660" + }, + { + "name": "cactus", + "unicode": "1F335", + "digest": "e87588e6548d201db903dc0523b3ccc83c6b559981d743eae1504ce668cd8be4" + }, + { + "name": "cake", + "unicode": "1F370", + "digest": "3947783d128018f5e396602d0492cb5c31e8e8df98af01eda7cade71aea8d989" + }, + { + "name": "calculator", + "unicode": "1F5A9", + "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" + }, + { + "name": "calendar", + "unicode": "1F4C6", + "digest": "00bb700dd88efbc43bc64263491cdf77965130b1dc23f31e682905c3dfe4040c" + }, + { + "name": "calendar_spiral", + "unicode": "1F5D3", + "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" + }, + { + "name": "calling", + "unicode": "1F4F2", + "digest": "2375828085f2efd17b8a5ebb3cfec1e420190913328a7a0dd9ff0f67c7249ffb" + }, + { + "name": "camel", + "unicode": "1F42B", + "digest": "9ff789ab50b51cd9e7fdc7fbe8d6f913fda95dfd425949f97974548652a53ce1" + }, + { + "name": "camera", + "unicode": "1F4F7", + "digest": "d95192b9ba0f566d8874099125def031e15297d1306989ea9b6a49f7b9b56661" + }, + { + "name": "camera_with_flash", + "unicode": "1F4F8", + "digest": "4db6fb3fdb9a004537dff97f4197c7ed87c9c978ba9ac562ed8bb7c1fa260d38" + }, + { + "name": "camping", + "unicode": "1F3D5", + "digest": "f0855dc78bf6f3d06b3c2fc19180c8ff23d9e22871658fcc26a8fde08d328a0a" + }, + { + "name": "cancellation_x", + "unicode": "1F5D9", + "digest": "cea2f7a48543207615ee06755ded62c2a95a7eaf7d7b68a3fc25e74d94e2c92c" + }, + { + "name": "cancer", + "unicode": "264B", + "digest": "b990f85e9f62017d99526244eaef5c5e56f8808698011e85d44de1d2ed87f1a2" + }, + { + "name": "candle", + "unicode": "1F56F", + "digest": "5eefd555951e65298583009a307acc6fb6d02c88325ef3adf231717e75e5a333" + }, + { + "name": "candy", + "unicode": "1F36C", + "digest": "f14203c408173fbb94b4ee69d6de67226a17dc51b0cbd776f62623ee03fd2eb3" + }, + { + "name": "capital_abcd", + "unicode": "1F520", + "digest": "2a7cc876218b8c244b9802448ee25ce5004671a4f00ea950a636d8c3b766dbef" + }, + { + "name": "capricorn", + "unicode": "2651", + "digest": "03a5fd064c10f47c7fd0ae318c573bb559c269b1b2d61b45aa5b8ce9b5fbd9df" + }, + { + "name": "card_box", + "unicode": "1F5C3", + "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" + }, + { + "name": "card_index", + "unicode": "1F4C7", + "digest": "150950903eccb468981c58b87ed7c1ba44e17f52627d695f660ce96b3d9d6e8e" + }, + { + "name": "carousel_horse", + "unicode": "1F3A0", + "digest": "d6862085550fa139a147dceb1b2b9f950a08dcd01cecd8b8697f9c7992ca054e" + }, + { + "name": "cartridge", + "unicode": "1F5AD", + "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" + }, + { + "name": "cat", + "unicode": "1F431", + "digest": "002208c0c9165971853ee05cd05513175a913376a462a345a939d73401c6acb7" + }, + { + "name": "cat2", + "unicode": "1F408", + "digest": "fbdb726cc035f83784dcfe2d9adb85f8aeec429064aed5c5ca0b8be406068aa5" + }, + { + "name": "cd", + "unicode": "1F4BF", + "digest": "bd4d4eef2cc0b1e4ee1f5280f922743e76f27d35836987801b2b48969eac17d8" + }, + { + "name": "celtic_cross", + "unicode": "1F548", + "digest": "187aac988d7e02085a15f31c4cc0ff25127be5b088e354e65c7b1152bffb40ff" + }, + { + "name": "chains", + "unicode": "26D3", + "digest": "a6a915d9c361e1564e13cf2d33ad5df3d684aa349b8dc5909e6343d67401beb9" + }, + { + "name": "champagne", + "unicode": "1F37E", + "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" + }, + { + "name": "chart", + "unicode": "1F4B9", + "digest": "9fd5f8cd99988bbe0fabc89a0b23e28d1468641d2f9468e82b7148a1948d8236" + }, + { + "name": "chart_with_downwards_trend", + "unicode": "1F4C9", + "digest": "6fe456d76c0a996c12049057b5d60129098a9deddfa2d133cff5c4400e4595a0" + }, + { + "name": "chart_with_upwards_trend", + "unicode": "1F4C8", + "digest": "e83cc4cf4228bd77e030a19755b11cf75cf671f40973c23e240afa54d9de478e" + }, + { + "name": "checkered_flag", + "unicode": "1F3C1", + "digest": "77501c2c66af31f72f5c05f21e87598cd59740b5cfc02926c66dc755bab3c3cf" + }, + { + "name": "cheese", + "unicode": "1F9C0", + "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" + }, + { + "name": "cherries", + "unicode": "1F352", + "digest": "5a0ba73039e4b56e3d16a1c70ad992f41af7a16f6d5ba4b5337bdf338276f0ff" + }, + { + "name": "cherry_blossom", + "unicode": "1F338", + "digest": "b40533225291f539ffe97e4ab1d70d07e179b2f9345b2814355164d0407cf3bf" + }, + { + "name": "chestnut", + "unicode": "1F330", + "digest": "6a2a37899d28326daf36965b343b2646492c2c0cee8871321cc17315d6252a9a" + }, + { + "name": "chicken", + "unicode": "1F414", + "digest": "13d770684a11ea10c0ae7570a98c5dfafd4bfb78ac3f72f46729aef9060b85c0" + }, + { + "name": "children_crossing", + "unicode": "1F6B8", + "digest": "654d2502c1edc57c5ab4237df76db3121f6b8735eb13d30bffd305605a083445" + }, + { + "name": "chipmunk", + "unicode": "1F43F", + "digest": "1ae3c838450afcbbe8a96992481dde252e343ab83546d0789ebed81a78ca9188" + }, + { + "name": "chocolate_bar", + "unicode": "1F36B", + "digest": "2486b7265048eb2294d6be0a0a8a4d6067df95721ace9d131d8f715a27ba8cf0" + }, + { + "name": "christmas_tree", + "unicode": "1F384", + "digest": "454c08870eaa84283c19731ed3b10c4868d2e2f0cc44f2feba0de9ba4cc9c4e1" + }, + { + "name": "church", + "unicode": "26EA", + "digest": "b62e838ffb0dfefeced1707359437b6815e0721783b549212282e08617402f6f" + }, + { + "name": "cinema", + "unicode": "1F3A6", + "digest": "6df56f6a0008d0352740d1e045ffdb702e80c2a6d88b6db1a8bcd27eb3c12dcc" + }, + { + "name": "circus_tent", + "unicode": "1F3AA", + "digest": "f8b7a7f4cf4f9efd20423acc30abb3a28e2a5183b3e39f5cc88e7e0ed7757d64" + }, + { + "name": "city_dusk", + "unicode": "1F306", + "digest": "8779066dc9386d05c951b1df1753983c2937a5f3b84d5fc09ed0b172d4ef914e" + }, + { + "name": "city_sunset", + "unicode": "1F307", + "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" + }, + { + "name": "cityscape", + "unicode": "1F3D9", + "digest": "15251a708d50fc721bd67d8abb2a517c0bade196df3b736e21d79191d749241f" + }, + { + "name": "cl", + "unicode": "1F191", + "digest": "104591d8e7b980cf38dcf8326d36c845384b7a4e6d94c49f36e9946484712a95" + }, + { + "name": "clap", + "unicode": "1F44F", + "digest": "ed6ef8bb78ca1fa295b87222c440c6d5ba4f154f2752bf0d428941260d66aaac" + }, + { + "name": "clap_tone1", + "unicode": "1F44F-1F3FB", + "digest": "57a1fd1fa2578c30b8a47abb84e81af5f5bbc6c301a5daf0c53d4d07b017e777" + }, + { + "name": "clap_tone2", + "unicode": "1F44F-1F3FC", + "digest": "2ad4dcd513e55486f21151bf3792e1febf116574d238545b07b4290901430fdd" + }, + { + "name": "clap_tone3", + "unicode": "1F44F-1F3FD", + "digest": "2d8c705d4fcc162fb65cd51e2c6683f1129ebc72fba13343533f64ede1c62687" + }, + { + "name": "clap_tone4", + "unicode": "1F44F-1F3FE", + "digest": "40ffd41b2b4f59d0040e9d20497e57c4e47f18aeae43fcae02be5c2f50069102" + }, + { + "name": "clap_tone5", + "unicode": "1F44F-1F3FF", + "digest": "be55df1ac7600ba086c2ef6ea223ebc62271fa47876c53ade1a1c0151fdc994c" + }, + { + "name": "clapper", + "unicode": "1F3AC", + "digest": "a8748398f56fd2c1e6e87fe0c77edec444df7c7dd462d43dbcea6d8de97c81c5" + }, + { + "name": "classical_building", + "unicode": "1F3DB", + "digest": "6a607b0666141b51d6e944b04f3f6188a5c026396e6105f1d2a5e6b6350cd66b" + }, + { + "name": "clipboard", + "unicode": "1F4CB", + "digest": "4ca1a0b864a962b111d6bdb65373b779f3fff571ffd32d029666f9b708e1ab73" + }, + { + "name": "clock", + "unicode": "1F570", + "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" + }, + { + "name": "clock1", + "unicode": "1F550", + "digest": "c0550fa0c385920cbdb775bdaaa5e812097a484c4a32e35ebbafe3a364a4a438" + }, + { + "name": "clock10", + "unicode": "1F559", + "digest": "25651ac5520505f326457364428de3679cc22ca57278d4c54cc4b60420fa7b74" + }, + { + "name": "clock1030", + "unicode": "1F565", + "digest": "dbf682bac968fc5a3959af2b96eaaa5ee78306f6341c43c1345b94bc561a3d04" + }, + { + "name": "clock11", + "unicode": "1F55A", + "digest": "333732dd6c3184f257964bcf5a20a6111f9adb04560b5d12dc613636e846df5b" + }, + { + "name": "clock1130", + "unicode": "1F566", + "digest": "005999cb37998adea1645d7df63b2705a42db3b4f1a734891d79af3e833764ff" + }, + { + "name": "clock12", + "unicode": "1F55B", + "digest": "6690e591bec1751e1c5472e0bf52f66779b2113e5b8c6c578e65dbb83d091b16" + }, + { + "name": "clock1230", + "unicode": "1F567", + "digest": "549f3921bcff7f330c5a41e6756d8c15601f1f8278b35b369148771c60be2a6f" + }, + { + "name": "clock130", + "unicode": "1F55C", + "digest": "9332ef07a9dde8ccaa1e58a3e97edee0601a1152fc6d351b782816c838d2a408" + }, + { + "name": "clock2", + "unicode": "1F551", + "digest": "9d1ec8fbdae627880e1c067c10d6a40f1e4494a246c77224b3cd7b287554c4b4" + }, + { + "name": "clock230", + "unicode": "1F55D", + "digest": "3578a39c28695d4e617a648a1eb44e0bb5a8a11dcbe04fa2eb2aea0a60589067" + }, + { + "name": "clock3", + "unicode": "1F552", + "digest": "c2e2a27301b6ac27dc359be590448eb1e65fe87211f1af30a473d8bde4f3db47" + }, + { + "name": "clock330", + "unicode": "1F55E", + "digest": "7a77cf8cf9a98f4767a2dca1d3795be45938eee185db81120d85cedebe128899" + }, + { + "name": "clock4", + "unicode": "1F553", + "digest": "0945c4199400d546350cfff25bc9e9160789d1cf9890b3318bdc462ac6cc9782" + }, + { + "name": "clock430", + "unicode": "1F55F", + "digest": "9fdb6f1fa076c4c6a395dbf6db27499ee447b3558f3aa64d913686c360e428a8" + }, + { + "name": "clock5", + "unicode": "1F554", + "digest": "855b3500eb6d20bb6e51d3a6c9d1a5131c06404c6c149841c7cca52201036428" + }, + { + "name": "clock530", + "unicode": "1F560", + "digest": "a6ebd9f884d45a1f43650351a1f1da9724bc044d7da2f6d99ffb3d1fa0c31c5d" + }, + { + "name": "clock6", + "unicode": "1F555", + "digest": "e38f9fc4f87f12ee602dcf2285d59dbc343fc0fc37662992cfe9866c20f58e87" + }, + { + "name": "clock630", + "unicode": "1F561", + "digest": "735954a650791fc38c845c43998023e652d36e55534850e43952878b8804b2f1" + }, + { + "name": "clock7", + "unicode": "1F556", + "digest": "2c4244ec4019e9624e6ea5a751bb735ab87bead33b1ea160265c81bba3c2f736" + }, + { + "name": "clock730", + "unicode": "1F562", + "digest": "0bcf20e30be1bb23394696770301867e307f8e5014e0ed7d75ed96efe34d625d" + }, + { + "name": "clock8", + "unicode": "1F557", + "digest": "af454047a1765ef1c8355969302a826d4c47f5c61a6ec47fdec3510a8003b0d8" + }, + { + "name": "clock830", + "unicode": "1F563", + "digest": "e48b81dac055dc6d5f7832cf34368329c573d03b35bfe076fed1c6e6d48a82e7" + }, + { + "name": "clock9", + "unicode": "1F558", + "digest": "f2a3d1bc029dc0e6406cdaa96542e77503e4cfb79d99c69cb454b8cf635a73fc" + }, + { + "name": "clock930", + "unicode": "1F564", + "digest": "bb1b2b83052e8e6fb97c48c13bce0d950907e044eb2dabf21d7fed321f75110b" + }, + { + "name": "clockwise_arrows", + "unicode": "1F5D8", + "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" + }, + { + "name": "closed_book", + "unicode": "1F4D5", + "digest": "afd6dae5fa0f59330fc2adb922e92b3410a33a80a2667651718c7dac588010bc" + }, + { + "name": "closed_lock_with_key", + "unicode": "1F510", + "digest": "d0ed5c00f939111ce86f9c741b733b22e04ebbd871aa33da3eb0f46a6f38b707" + }, + { + "name": "closed_umbrella", + "unicode": "1F302", + "digest": "3ef08b299f9170007a5433fe82d0953bf0f75b6685d0ce58972f9af032dc471a" + }, + { + "name": "cloud", + "unicode": "2601", + "digest": "d1e7932551e85c6e86bfb3b41f0c936a6d0953bf9f9119b8cca3eaed22ac0c01" + }, + { + "name": "cloud_lightning", + "unicode": "1F329", + "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" + }, + { + "name": "cloud_rain", + "unicode": "1F327", + "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" + }, + { + "name": "cloud_snow", + "unicode": "1F328", + "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" + }, + { + "name": "cloud_tornado", + "unicode": "1F32A", + "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" + }, + { + "name": "clubs", + "unicode": "2663", + "digest": "5fd19fadd3b0887a6a59819ffbbe33a061055c043200700c31be30e14a5d36d5" + }, + { + "name": "cocktail", + "unicode": "1F378", + "digest": "cf096ebe15b4053702d490cd96f04d565b4993529bcd6d8d50cb821200d1cd92" + }, + { + "name": "coffee", + "unicode": "2615", + "digest": "6ea6128e353d9f74aee99caaaaa30c53f996fb242bf3bffb0fa92e6b4d373e57" + }, + { + "name": "coffin", + "unicode": "26B0", + "digest": "b59772d7aa262c4d7433f9cdf76d50011f4c63421b730c8ab4a08675f730c39f" + }, + { + "name": "cold_sweat", + "unicode": "1F630", + "digest": "f0d0057bf01db8d930f6e4632c5bf8d0b1bc709bcfb6463a1f1973b5f1d70a83" + }, + { + "name": "comet", + "unicode": "2604", + "digest": "00252ec55d1846d95c8d4c704b35251232d9810029fc215a7da08262dd1f3541" + }, + { + "name": "compression", + "unicode": "1F5DC", + "digest": "432fbe66e5e3c38ebfeb4eb03465667a1e1be868b4afe510ec95eadda6481bde" + }, + { + "name": "computer", + "unicode": "1F4BB", + "digest": "99777be010488867c7872b2e235be7c35b1a6f28d92baa921b61ced5491c0257" + }, + { + "name": "computer_old", + "unicode": "1F5B3", + "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" + }, + { + "name": "confetti_ball", + "unicode": "1F38A", + "digest": "e77d0c0970d3d12e123e548639fc0fa3ce41668667e4be55baefc09dfaa22cb0" + }, + { + "name": "confounded", + "unicode": "1F616", + "digest": "0f51db64149151d3d7ae5dce08c9af3d064123524fa36fe1f51a78cbd966b6ea" + }, + { + "name": "confused", + "unicode": "1F615", + "digest": "ed23587432c1be98356156784ca4fe0b374b7b3b371660d45cfb0a1efd44e322" + }, + { + "name": "congratulations", + "unicode": "3297", + "digest": "2a46d640bf24fd4dc7649baf4b28c4adb30eda8d24d70eda07036c85b48195e0" + }, + { + "name": "construction", + "unicode": "1F6A7", + "digest": "73fac9fb5eb91954b0f998f9d05fb953241eed988c134fa42477393159fa34fa" + }, + { + "name": "construction_site", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, + { + "name": "construction_worker", + "unicode": "1F477", + "digest": "2be436fa7ad0a31e328fc6f776044bd1eec35c99541ced891792e3bef738d0a0" + }, + { + "name": "construction_worker_tone1", + "unicode": "1F477-1F3FB", + "digest": "172cebc84f91237a85292c5ab0a105cc3abbb96e7423c4ae81feffd00bdb3b26" + }, + { + "name": "construction_worker_tone2", + "unicode": "1F477-1F3FC", + "digest": "3e9b96ddfd639eefda99ad3a0ad26a28a0f2c8be72988c2bdbd648e6104638b6" + }, + { + "name": "construction_worker_tone3", + "unicode": "1F477-1F3FD", + "digest": "11f83c565168dce5ac2387b873769d85ec4087171d6e92fc766c209ea06cd4f3" + }, + { + "name": "construction_worker_tone4", + "unicode": "1F477-1F3FE", + "digest": "09e320e78e3a2940f0c5a0ef9a235ab72c51e053fd8ff433843fdb62571c8e70" + }, + { + "name": "construction_worker_tone5", + "unicode": "1F477-1F3FF", + "digest": "7ac2a1a0038e7aefea889380be604a98255823587e90799165f7db39dd03a0cc" + }, + { + "name": "control_knobs", + "unicode": "1F39B", + "digest": "9f10e578b410ff6aa7cc7fe806a0f1181893765303c0ca3867b652f1392a8a22" + }, + { + "name": "contruction_site", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, + { + "name": "convenience_store", + "unicode": "1F3EA", + "digest": "1ff4351e4a4503f58ed5d35074a2112c681337e35ffe55332187481685573606" + }, + { + "name": "cookie", + "unicode": "1F36A", + "digest": "5c78ce2e721b0a3767d6ce0b59c1e88fdf94a7edc94e98c4d6b7aadb5b2aeea7" + }, + { + "name": "cool", + "unicode": "1F192", + "digest": "54a96697a5070388ce8364a5ee2e0d78a53acc8b4f6755b1359fd67252cc41e8" + }, + { + "name": "cop", + "unicode": "1F46E", + "digest": "16bee252c2a133bcf57f6d7b8372a61364744a2f662acb90e2005732555135fa" + }, + { + "name": "cop_tone1", + "unicode": "1F46E-1F3FB", + "digest": "2fc52f3ed735e327d12dadb15f9feb7b7f720fc6857b551548a2a84809053817" + }, + { + "name": "cop_tone2", + "unicode": "1F46E-1F3FC", + "digest": "6208f3174ced4f07ba3820ba838b247d7438d69d86eb04927333e7436e56af7e" + }, + { + "name": "cop_tone3", + "unicode": "1F46E-1F3FD", + "digest": "2427d30bdfe127be4d8c3870472cae191eece142c784a5c2809df938f43e7c53" + }, + { + "name": "cop_tone4", + "unicode": "1F46E-1F3FE", + "digest": "6e73f8abdf816f3cb2728b971a5a8d006a236c1d71b2ee1788ab60329f406323" + }, + { + "name": "cop_tone5", + "unicode": "1F46E-1F3FF", + "digest": "4b146465cc95ade7e9ca722e31a1b06311214dae8f7f4d95c6329d56c45b451f" + }, + { + "name": "copyright", + "unicode": "00A9", + "digest": "8143583821085dfc8ac21079fe220288ba3a3b6ca3014dc5dc98b18da77589c1" + }, + { + "name": "corn", + "unicode": "1F33D", + "digest": "0160502226b5f9af81763545f288dbbb20632039d7509f347c751cfdb49dc5b5" + }, + { + "name": "couch", + "unicode": "1F6CB", + "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" + }, + { + "name": "couple", + "unicode": "1F46B", + "digest": "97fe611a613216a1788f9bd88a9deb4714ee123a66b5fd3d0ac916fbb4da7304" + }, + { + "name": "couple_mm", + "unicode": "1F468-2764-1F468", + "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" + }, + { + "name": "couple_with_heart", + "unicode": "1F491", + "digest": "d9701173a5e8dff052ab6a15a42494dbb61dc7146d3734c82916abc9c05f76db" + }, + { + "name": "couple_ww", + "unicode": "1F469-2764-1F469", + "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" + }, + { + "name": "couplekiss", + "unicode": "1F48F", + "digest": "e722730de82397da7c8f88d79319b391e8f01fbe4a9133850cc92ad34e77bd82" + }, + { + "name": "cow", + "unicode": "1F42E", + "digest": "dcc1efef2f02588806a156ed43da959c587d4c576ff6badec77f820ed3ba507f" + }, + { + "name": "cow2", + "unicode": "1F404", + "digest": "dcf59f92fd0a37b2ca720bcda606defa4357b58d8f4ad15c1288ad8d814b2bc7" + }, + { + "name": "crab", + "unicode": "1F980", + "digest": "59d34a4e92326ebeab188d9e33b25c20f4d54d187c274713fa3256b03b9e662a" + }, + { + "name": "crayon", + "unicode": "1F58D", + "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" + }, + { + "name": "credit_card", + "unicode": "1F4B3", + "digest": "708c0e7008e06e5d1b3b4e68a7e0ada9f4ae22ab6c28285d81a340f913fd9a84" + }, + { + "name": "crescent_moon", + "unicode": "1F319", + "digest": "0959f838a410e8bfeebf00aa9658df56e515dbd2361142021071e17244662bfc" + }, + { + "name": "cricket", + "unicode": "1F3CF", + "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" + }, + { + "name": "crocodile", + "unicode": "1F40A", + "digest": "99abcb42264d40d2450aaca8c3759a019bfd600a311cf3027243f1ca200d4639" + }, + { + "name": "cross", + "unicode": "271D", + "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" + }, + { + "name": "cross_heavy", + "unicode": "1F547", + "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" + }, + { + "name": "cross_white", + "unicode": "1F546", + "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" + }, + { + "name": "crossbones", + "unicode": "1F571", + "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" + }, + { + "name": "crossed_flags", + "unicode": "1F38C", + "digest": "d4da057db289bec83f0106a94c89bd0cd9b52c7c7f8bc69bc8cbce480d53e12b" + }, + { + "name": "crossed_swords", + "unicode": "2694", + "digest": "f159978583fa77c73ba6de85d35c4195cbd55963e537bd2bfd8f98ab8ff3559a" + }, + { + "name": "crown", + "unicode": "1F451", + "digest": "e6fe2a28b7d80749ca121cabbe89321dcecdd760a122e73fb1562ea9bb40e90d" + }, + { + "name": "cruise_ship", + "unicode": "1F6F3", + "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" + }, + { + "name": "cry", + "unicode": "1F622", + "digest": "2d6a096796222c29b050f74db6b5aff9b9f61390c5eb56e45d1801918751002f" + }, + { + "name": "crying_cat_face", + "unicode": "1F63F", + "digest": "df057d4e3e5c5c87caedf87ea3a6f936811b93f228f46bb7018d2bb5afaa6d35" + }, + { + "name": "crystal_ball", + "unicode": "1F52E", + "digest": "7de438f88134c32c4db67d705e5fecf2a6187a87f56ebbb5bcc5ba09626e2935" + }, + { + "name": "cupid", + "unicode": "1F498", + "digest": "7cb3f7d1ddf9678982197ef0e65735fb465ae8e3652d611f37d3bcccf4d7e2c1" + }, + { + "name": "curly_loop", + "unicode": "27B0", + "digest": "881a43ae406cb74b2ef136bf970db9928bcdc3bbbb7393e90d2c597fe1dd9a96" + }, + { + "name": "currency_exchange", + "unicode": "1F4B1", + "digest": "c4d76e9e61fac8d3c0cb9e07f1fbf1a7fcac6f4d4c78776ff7f04fc9391ce689" + }, + { + "name": "curry", + "unicode": "1F35B", + "digest": "ebe41ee864c873e3a371888c0087b11dbcb124335812895002ed81fe2b6ba571" + }, + { + "name": "custard", + "unicode": "1F36E", + "digest": "afc192f405c30e2d529ec0f4b31a7faf474bcd01fded5294dc38880b8bb22155" + }, + { + "name": "customs", + "unicode": "1F6C3", + "digest": "5abb98151a79cebc1032c0ea149617093e42f41e50574a790a91074cabaa4c3a" + }, + { + "name": "cyclone", + "unicode": "1F300", + "digest": "ae77e15bf2f312f03dbc5c7813d304005bbb549953482db9beb91810c585dc0e" + }, + { + "name": "dagger", + "unicode": "1F5E1", + "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" + }, + { + "name": "dancer", + "unicode": "1F483", + "digest": "e050db55afbb968e02219a58c7e82b824848d299a4df64f0d08d4e1872816203" + }, + { + "name": "dancer_tone1", + "unicode": "1F483-1F3FB", + "digest": "350f6b2e4589fdd436173163035621b8da0bd49c7b9ec9f39593aae5e0ed0641" + }, + { + "name": "dancer_tone2", + "unicode": "1F483-1F3FC", + "digest": "a9efc84ec80582f286147ca34162a27fd5989f4030084acdbc309d4368660f5b" + }, + { + "name": "dancer_tone3", + "unicode": "1F483-1F3FD", + "digest": "ef187f44278fdb8605c80f5cf199e0b3de8a49085dada2e215bb91e1d7d3be5d" + }, + { + "name": "dancer_tone4", + "unicode": "1F483-1F3FE", + "digest": "5195bc352dc9d24cc5505a167c756038e55c05048c61799ea1bfdf2debe44ac2" + }, + { + "name": "dancer_tone5", + "unicode": "1F483-1F3FF", + "digest": "55cb7eee9fa11a16a3932800a19e334546f7396df6aadde22e58fe3185926b16" + }, + { + "name": "dancers", + "unicode": "1F46F", + "digest": "39e7dfd9dafeee20f2968960b1179ee4bf3f2b63a3035fc1944024d0ae8b5de1" + }, + { + "name": "dango", + "unicode": "1F361", + "digest": "2a1b50abe5dc72335344878d9b701028ccad651964d9e3affeedbf3c2bfd652a" + }, + { + "name": "dark_sunglasses", + "unicode": "1F576", + "digest": "6bb1e911a93d5eb0581d3ce8f8929125d3d8fc04e086f3263cfd25af1348ce6c" + }, + { + "name": "dart", + "unicode": "1F3AF", + "digest": "6f28741543a4c1eead21856128ffea1fcf772954fe6af40844dfde47f092ed32" + }, + { + "name": "dash", + "unicode": "1F4A8", + "digest": "25aef37611f1c2f2e96518bf8aeba80580dca9634c8505d390c147388adf6746" + }, + { + "name": "date", + "unicode": "1F4C5", + "digest": "de591b8fad608be761b839beefe9e4c2316320bcf0c44c543a1bc4b89923d938" + }, + { + "name": "deciduous_tree", + "unicode": "1F333", + "digest": "ff31a52096ac1eae770f7f71b6d802198add2c8b4d9d7c9327071b6d6ab86c7b" + }, + { + "name": "department_store", + "unicode": "1F3EC", + "digest": "c1e200d5fdd792121acabdb17bbcfe8e28a63757cfd895c72d4909f14de95ac2" + }, + { + "name": "descending_notes", + "unicode": "1F39D", + "digest": "f09c6a2e094b13bf91cc07b7b776e43348ccef9f91247ca36cc02e7d91098af0" + }, + { + "name": "desert", + "unicode": "1F3DC", + "digest": "e45815250bfc5411de516f87efa218874bcda4b0420b4c17182efc22ba0ce80d" + }, + { + "name": "desktop", + "unicode": "1F5A5", + "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" + }, + { + "name": "desktop_window", + "unicode": "1F5D4", + "digest": "d5b6c4a847e2a96f97f50fd353a22cb121915cb1d7bbc0f02df38769819b6b7e" + }, + { + "name": "diamond_shape_with_a_dot_inside", + "unicode": "1F4A0", + "digest": "4e0e6364b8682dec9a9e20676161c9c9c0faf0a5fdd5402ca2668b18f2bb850a" + }, + { + "name": "diamonds", + "unicode": "2666", + "digest": "42b13b2ed8e5fc63fbe81263c06cc203ba18a45ed5cc2a4fdbf617d219a0d3b4" + }, + { + "name": "disappointed", + "unicode": "1F61E", + "digest": "7f1a619fef84960a9f312d17a58aa58105a4f20a4072efb10227892ab22475d8" + }, + { + "name": "disappointed_relieved", + "unicode": "1F625", + "digest": "a389f5e0a4b619dbc406217967fb1f8f3d0e49b3f790e554ae0ececadbf98967" + }, + { + "name": "dividers", + "unicode": "1F5C2", + "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" + }, + { + "name": "dizzy", + "unicode": "1F4AB", + "digest": "d6fba9b906f0eabd46686e416273a2ca6634249374385f2abf7ed284f0eef995" + }, + { + "name": "dizzy_face", + "unicode": "1F635", + "digest": "b55e20c1551a2912bb5ec64a66c788c9d6f21594cc1da66032188f3814b03f40" + }, + { + "name": "do_not_litter", + "unicode": "1F6AF", + "digest": "126f8c4085e0a8de8241f211f96c3f42c3e3400ea7d8fdf79a14443c3eceb972" + }, + { + "name": "document", + "unicode": "1F5CE", + "digest": "2cbca96cc69306a10f1a9b6505723e027239439d899f6b395dc43f3c37d2d777" + }, + { + "name": "document_text", + "unicode": "1F5B9", + "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" + }, + { + "name": "dog", + "unicode": "1F436", + "digest": "c7b729de8a0967b1f38c3fa5ded94e77e329588caeaaf43abfd1090f420e62bf" + }, + { + "name": "dog2", + "unicode": "1F415", + "digest": "e1897ca60bb3d2662cbe7933352e2b9c50739adf5901d3328797bf399575b97a" + }, + { + "name": "dollar", + "unicode": "1F4B5", + "digest": "7db1e57f799439df1295d42b5249393f1e8cacc8df54caf30499c967a7282742" + }, + { + "name": "dolls", + "unicode": "1F38E", + "digest": "398e7ff5780328700aadded7ce8c50757b1096af5cec66cc4d813a6714686b6d" + }, + { + "name": "dolphin", + "unicode": "1F42C", + "digest": "27385af08848d93acdd13f72751074c2cbccb5ab3c6047e334598af74ed4862d" + }, + { + "name": "door", + "unicode": "1F6AA", + "digest": "3365d7834086328ecbf1da0037f1cf1d0eb49534e173f7962a9e8f4b2ab87e26" + }, + { + "name": "doughnut", + "unicode": "1F369", + "digest": "b4b99fdfe8d07b49cbdd78f8c57e4424819a4ffc8a3ba4867da44cbb3b3a5cca" + }, + { + "name": "dove", + "unicode": "1F54A", + "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" + }, + { + "name": "dragon", + "unicode": "1F409", + "digest": "d7d016568b54d67017681a075fb799d4a2a790ecfa2946d02dbcee629eb4975d" + }, + { + "name": "dragon_face", + "unicode": "1F432", + "digest": "4d0025f1df63b62448477a8f08a50704e15caafb10fea476b529113f41797ab9" + }, + { + "name": "dress", + "unicode": "1F457", + "digest": "02d56ed227280eaf5ad92830ee304afb81f74bb5a13c855397bcd04dd7fa51fb" + }, + { + "name": "dromedary_camel", + "unicode": "1F42A", + "digest": "5afe8a0b73f9f4560264020b1e02a566149dbc38c15a00d2fb5cd90b32d09a75" + }, + { + "name": "droplet", + "unicode": "1F4A7", + "digest": "a92c419792cbd3ba90ed21547362134cfac3e17a5304ee4e3872c9f7b561f834" + }, + { + "name": "dvd", + "unicode": "1F4C0", + "digest": "1ba23e2f01ced5e192e4c1d2f766d9bce400470e81c81410139fd3c0739422df" + }, + { + "name": "e-mail", + "unicode": "1F4E7", + "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" + }, + { + "name": "ear", + "unicode": "1F442", + "digest": "70ba1103a34e68590d91a3b6f8acdbad3b1c65e46e31e26ee1cb855c1e21095e" + }, + { + "name": "ear_of_rice", + "unicode": "1F33E", + "digest": "ddd5f3cc83dbdafd9115861eecd0128e52165bb1dd0049df06ffc564b650d384" + }, + { + "name": "ear_tone1", + "unicode": "1F442-1F3FB", + "digest": "72977be94f5d287a09d175f98fba8b7955ae13aa12ce8e029c0ca875c02ee820" + }, + { + "name": "ear_tone2", + "unicode": "1F442-1F3FC", + "digest": "5ff2e46cb3be7f13b8b94092246b58dab4c2a9ee2a5a46e0b84cf35a6928141f" + }, + { + "name": "ear_tone3", + "unicode": "1F442-1F3FD", + "digest": "19b523f5ada2acaea94b922059c458a3303f4da1dd4c197cf25d31a0e6ecc4b2" + }, + { + "name": "ear_tone4", + "unicode": "1F442-1F3FE", + "digest": "6a5cca9f49c539ef7d0883a2f39652f33ee2d3b25dca0234e4ba027ebbb2b466" + }, + { + "name": "ear_tone5", + "unicode": "1F442-1F3FF", + "digest": "a0a56e8abd36e9be6e2448bcee6f56ecb8bf62d728b19ab6e8f9c6338e226b67" + }, + { + "name": "earth_africa", + "unicode": "1F30D", + "digest": "d4921b543d7cf0c7344fa50c5e4d5a76c208d900be852adc1ee82ed4e8861a39" + }, + { + "name": "earth_americas", + "unicode": "1F30E", + "digest": "61691e6aa9b8d90fc7f75fbc6cc7add5c36022d38f3e05c9d7c54dc44cf865bb" + }, + { + "name": "earth_asia", + "unicode": "1F30F", + "digest": "262904cb552c7f5cf828a11071b3d430a74824b7464e8759ef93ee23b1705767" + }, + { + "name": "egg", + "unicode": "1F373", + "digest": "a7dd617cad489c481ffd14937d9ed491cdd5756903e00473f42600c2fbefb600" + }, + { + "name": "eggplant", + "unicode": "1F346", + "digest": "e5402e8ae5b7f9699ed86b97c242f7939d5731c5a364a2d5b9d04ea5d293cda1" + }, + { + "name": "eight", + "unicode": "0038-20E3", + "digest": "34e293d3228e4643a0132d592f96db91b651fe6ced056ac3c8a3fd49c5ed3416" + }, + { + "name": "eight_pointed_black_star", + "unicode": "2734", + "digest": "c3c2da75731a9a0f4f0a8d1f9cffef75c35e19b7f5d4081da33ac12b46be5fc2" + }, + { + "name": "eight_spoked_asterisk", + "unicode": "2733", + "digest": "cc69618c1074d2b00e6f2c49df5e2c5ff6f4c0fae305505eb8c9daa65a0ea340" + }, + { + "name": "electric_plug", + "unicode": "1F50C", + "digest": "732e1d1675233a0b4643cb73d0c352f8a5a56a11ee90d26627ad1e43c2e4a8e5" + }, + { + "name": "elephant", + "unicode": "1F418", + "digest": "08df3910c4d5d8f49a72c47dd938195e495bde8fd8b3e7b17098a2c1afc41634" + }, + { + "name": "end", + "unicode": "1F51A", + "digest": "05844ab9dcb43deff86f04617af6ea09215595de1415dcfaae018bced57938fe" + }, + { + "name": "envelope", + "unicode": "2709", + "digest": "aad272511d0db910437ba25cf1fb9c806d47aad92a232edb87055916daf4676a" + }, + { + "name": "envelope_back", + "unicode": "1F582", + "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" + }, + { + "name": "envelope_flying", + "unicode": "1F585", + "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" + }, + { + "name": "envelope_stamped", + "unicode": "1F583", + "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" + }, + { + "name": "envelope_stamped_pen", + "unicode": "1F586", + "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" + }, + { + "name": "envelope_with_arrow", + "unicode": "1F4E9", + "digest": "c1ba19b5e7cf64c547ac46eee139e6af70700d49ab511a96e6828c30feb116bc" + }, + { + "name": "euro", + "unicode": "1F4B6", + "digest": "f571952583ffecfa5777065e4d1b680c423d25bc80e567a48fb5d7a1c1b5e735" + }, + { + "name": "european_castle", + "unicode": "1F3F0", + "digest": "db82e383975d079a7bb006e7868035088d75c33bd4031cf8466b71089b65426f" + }, + { + "name": "european_post_office", + "unicode": "1F3E4", + "digest": "d9b38e0f0ca3ad8895b40c767bdbb2b142ccaf03a86c2f275f57a31ed478801a" + }, + { + "name": "evergreen_tree", + "unicode": "1F332", + "digest": "60d8b2d86b20255341f7ecad6d0f178ba9db5fa6b3de92f1b439cdb19f2fc0b1" + }, + { + "name": "exclamation", + "unicode": "2757", + "digest": "cd900ecf82de2b26f0d7783dac4b3232ae94d2cddad5bfacea2eaf65b7ac0a09" + }, + { + "name": "expressionless", + "unicode": "1F611", + "digest": "2ec9466b2d629907ce4c3e24e57f7ee556d2258ff011d972e14d0ae969a40c51" + }, + { + "name": "eye", + "unicode": "1F441", + "digest": "790841e8fce647173eec3c5019440ad9c7e916c535f92acb3132bd92df148cad" + }, + { + "name": "eye_in_speech_bubble", + "unicode": "1F441-1F5E8", + "digest": "bcde5a89a7653bff302685d9d632dd2723796a7ac73125fb7b9493d1ca848e0a" + }, + { + "name": "eyeglasses", + "unicode": "1F453", + "digest": "fd140bef19c420bafe59368d35dd58a58a53e7145b104bae94be10f90679213b" + }, + { + "name": "eyes", + "unicode": "1F440", + "digest": "57ed1f87ebe2485ea32ea69abdb8c5f7ccdcc149b33e74230d801f0883c68c5d" + }, + { + "name": "factory", + "unicode": "1F3ED", + "digest": "6e6b35ae013e5dd26852c9a95d05c39e89c1c1950a33f47e7b951c34af18f37c" + }, + { + "name": "fallen_leaf", + "unicode": "1F342", + "digest": "28ba8628065ffa973b525dd1455691c828d49c2b8c814af387880c13f6707f7e" + }, + { + "name": "family", + "unicode": "1F46A", + "digest": "b5307f86e54cfea581e8406f4b95c801e250a893a9d208cc9a69a6d910b90932" + }, + { + "name": "family_mmb", + "unicode": "1F468-1F468-1F466", + "digest": "49a753c3fcd4420800dd1cda585dae6bfa81615ad4862b477246456f86dc9e82" + }, + { + "name": "family_mmbb", + "unicode": "1F468-1F468-1F466-1F466", + "digest": "882a3a0048efd666b0ab3a07b9f08041aa3a2acdab02664d0feff30bbfa70d68" + }, + { + "name": "family_mmg", + "unicode": "1F468-1F468-1F467", + "digest": "45dd75c19d260a658c8ac93cf878976b96d2000f0efc9c59e72dacc80afb08fa" + }, + { + "name": "family_mmgb", + "unicode": "1F468-1F468-1F467-1F466", + "digest": "910f44a348a951d36ee1f1484d237085bec5083c3875a4d908831dfc64530eaf" + }, + { + "name": "family_mmgg", + "unicode": "1F468-1F468-1F467-1F467", + "digest": "012e75ad0d1b16c2ce63bf80a1ebfb1fc194229cfaf1241039599b82832f6aee" + }, + { + "name": "family_mwbb", + "unicode": "1F468-1F469-1F466-1F466", + "digest": "049a32f61c54f093d2124e25f8b2ec7eac13161e2f2ebf6dc067797698cbe831" + }, + { + "name": "family_mwg", + "unicode": "1F468-1F469-1F467", + "digest": "ba32c637caba634bda99ccba2a1a2a4b6f33aaaed933c30c7d5a51e8de1790d0" + }, + { + "name": "family_mwgb", + "unicode": "1F468-1F469-1F467-1F466", + "digest": "198faba987f45429329b93bbce4f111329f284558bf0eecfa1424186b5f009fe" + }, + { + "name": "family_mwgg", + "unicode": "1F468-1F469-1F467-1F467", + "digest": "3fa2e57cba314dcff04cf8186914823e1e081aabf34fa7437b05c58015df400c" + }, + { + "name": "family_wwb", + "unicode": "1F469-1F469-1F466", + "digest": "b9592fc110a25a478569075deaa520308ef74579cd47aa44df9836599d68143f" + }, + { + "name": "family_wwbb", + "unicode": "1F469-1F469-1F466-1F466", + "digest": "88f398997835fcf5153f17f6baf0deeb2a9c25ce2f8422192c18ac23e90b3193" + }, + { + "name": "family_wwg", + "unicode": "1F469-1F469-1F467", + "digest": "c8d859d3c957fe0d535efccde295fe99bab76e3d28ab5a49c8e736608461cb2e" + }, + { + "name": "family_wwgb", + "unicode": "1F469-1F469-1F467-1F466", + "digest": "006506e4a3d0c82642a0c8481ce95e5e3b969e20fe2def0a16dd686afddbc705" + }, + { + "name": "family_wwgg", + "unicode": "1F469-1F469-1F467-1F467", + "digest": "2553f0deab133aad09b99411d9dd68b56fede30f55ee1f354358767765e36673" + }, + { + "name": "fast_forward", + "unicode": "23E9", + "digest": "1baaed10969b60c083da754ee056bb71df36182cc65af40640acfb76f6b39200" + }, + { + "name": "fax", + "unicode": "1F4E0", + "digest": "b0a392192d03bd5d1ad5ee8eea933cf64725b1776819537bbed27561d78192e7" + }, + { + "name": "fearful", + "unicode": "1F628", + "digest": "7c4cc4de3357c2a6d6e779342b09dabb3ef832a32f2778a0ba074b446f588e8f" + }, + { + "name": "feet", + "unicode": "1F43E", + "digest": "cae13fb54ec64dbcf86ea25bebe2b79877e2d4f5d810b867f095f1d3dfc7f144" + }, + { + "name": "ferris_wheel", + "unicode": "1F3A1", + "digest": "a710a8a0fb039d953313b75330db37e3228d856593547b1f04dc83c00168b987" + }, + { + "name": "ferry", + "unicode": "26F4", + "digest": "21ea239b5adb68dc1ce6c5a1993b0a0b835ef6cc7a0a27cb890838d8475504f6" + }, + { + "name": "field_hockey", + "unicode": "1F3D1", + "digest": "1e46c7f0b5b79c90a5d211ea14cd7e358b1a26a3c8294439253f2b08d0e5c92e" + }, + { + "name": "file_cabinet", + "unicode": "1F5C4", + "digest": "c0b7bdab6c98909eb0fbf1ac89da0008bb00ddb1cb57fe64b4a5ac993eeb18c9" + }, + { + "name": "file_folder", + "unicode": "1F4C1", + "digest": "d98f93c6d7283df0c45f08d3d31ecf5b91b6db1b735959f19e42bfada500a0d1" + }, + { + "name": "film_frames", + "unicode": "1F39E", + "digest": "754a0a60e978f8299a0c4f8959e1f9260f01683e15ae943db430036f01a79b18" + }, + { + "name": "finger_pointing_down", + "unicode": "1F597", + "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" + }, + { + "name": "finger_pointing_down2", + "unicode": "1F59F", + "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" + }, + { + "name": "finger_pointing_left", + "unicode": "1F598", + "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" + }, + { + "name": "finger_pointing_right", + "unicode": "1F599", + "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" + }, + { + "name": "finger_pointing_up", + "unicode": "1F59E", + "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" + }, + { + "name": "fire", + "unicode": "1F525", + "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" + }, + { + "name": "fire_engine", + "unicode": "1F692", + "digest": "3ae03fa34a7088ada95458eb4ee3e97691b3489149f6bbc168086f0483ed3bb2" + }, + { + "name": "fire_engine_oncoming", + "unicode": "1F6F1", + "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" + }, + { + "name": "fireworks", + "unicode": "1F386", + "digest": "3dee83a27c406960253ca1460eb88a599c7b81506051b69605a421b17fe8282c" + }, + { + "name": "first_quarter_moon", + "unicode": "1F313", + "digest": "8fa066362d77bd889090bbe0904ca47f34704e29781c67133c6eaa521c3e1972" + }, + { + "name": "first_quarter_moon_with_face", + "unicode": "1F31B", + "digest": "8877edb366f8eaa00fd83200acf5a17c3b84d246a250519d565dda3aea866ec3" + }, + { + "name": "fish", + "unicode": "1F41F", + "digest": "9ce742108794cc15e59f7719623ae938efbd8155c93ad72585a32f4e32ea9414" + }, + { + "name": "fish_cake", + "unicode": "1F365", + "digest": "1b5b14509287e30da9b8d7abcec376b247f9095aea4bf3fc320349f061a4c321" + }, + { + "name": "fishing_pole_and_fish", + "unicode": "1F3A3", + "digest": "35db56776db1fcec7c8479922d57d54da2577cfe44a894bfd78c51c950c450fb" + }, + { + "name": "fist", + "unicode": "270A", + "digest": "6b80ac2e4d8b830ae06f7c1626d456460094e4ba20c20fb82dabb6b3d2ce7605" + }, + { + "name": "fist_tone1", + "unicode": "270A-1F3FB", + "digest": "d7c79f4f988dd68f064baa5a3a568ab299f8d409db45c8463f39b80e5dd6081f" + }, + { + "name": "fist_tone2", + "unicode": "270A-1F3FC", + "digest": "d1108194e2d962f9ccd00131876d769a8e003117a460d18b2ccbf93e0a0ea346" + }, + { + "name": "fist_tone3", + "unicode": "270A-1F3FD", + "digest": "12f5644b632c95a5c2e41cc9af299e286e266db8b3860091ef5be5f0c4ccc026" + }, + { + "name": "fist_tone4", + "unicode": "270A-1F3FE", + "digest": "521a3ac573381f3bc37a08ddd2d122767aaa0b6b7a38050d3671a12343351816" + }, + { + "name": "fist_tone5", + "unicode": "270A-1F3FF", + "digest": "604e5a234da1b9160e506b3c9026faf9e04268fced7b44baa1ef5e3d4efa83a4" + }, + { + "name": "five", + "unicode": "0035-20E3", + "digest": "0cbd6cd11eb6c2d67749112750d125f4f0a07b53bb7bfb1de0986d943ea9d632" + }, + { + "name": "flag_ac", + "unicode": "1F1E6-1F1E8", + "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" + }, + { + "name": "flag_ad", + "unicode": "1F1E6-1F1E9", + "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" + }, + { + "name": "flag_ae", + "unicode": "1F1E6-1F1EA", + "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" + }, + { + "name": "flag_af", + "unicode": "1F1E6-1F1EB", + "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" + }, + { + "name": "flag_ag", + "unicode": "1F1E6-1F1EC", + "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" + }, + { + "name": "flag_ai", + "unicode": "1F1E6-1F1EE", + "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" + }, + { + "name": "flag_al", + "unicode": "1F1E6-1F1F1", + "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" + }, + { + "name": "flag_am", + "unicode": "1F1E6-1F1F2", + "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" + }, + { + "name": "flag_ao", + "unicode": "1F1E6-1F1F4", + "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" + }, + { + "name": "flag_aq", + "unicode": "1F1E6-1F1F6", + "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" + }, + { + "name": "flag_ar", + "unicode": "1F1E6-1F1F7", + "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" + }, + { + "name": "flag_as", + "unicode": "1F1E6-1F1F8", + "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" + }, + { + "name": "flag_at", + "unicode": "1F1E6-1F1F9", + "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" + }, + { + "name": "flag_au", + "unicode": "1F1E6-1F1FA", + "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" + }, + { + "name": "flag_aw", + "unicode": "1F1E6-1F1FC", + "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" + }, + { + "name": "flag_ax", + "unicode": "1F1E6-1F1FD", + "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" + }, + { + "name": "flag_az", + "unicode": "1F1E6-1F1FF", + "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" + }, + { + "name": "flag_ba", + "unicode": "1F1E7-1F1E6", + "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" + }, + { + "name": "flag_bb", + "unicode": "1F1E7-1F1E7", + "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" + }, + { + "name": "flag_bd", + "unicode": "1F1E7-1F1E9", + "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" + }, + { + "name": "flag_be", + "unicode": "1F1E7-1F1EA", + "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" + }, + { + "name": "flag_bf", + "unicode": "1F1E7-1F1EB", + "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" + }, + { + "name": "flag_bg", + "unicode": "1F1E7-1F1EC", + "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" + }, + { + "name": "flag_bh", + "unicode": "1F1E7-1F1ED", + "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" + }, + { + "name": "flag_bi", + "unicode": "1F1E7-1F1EE", + "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" + }, + { + "name": "flag_bj", + "unicode": "1F1E7-1F1EF", + "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" + }, + { + "name": "flag_bl", + "unicode": "1F1E7-1F1F1", + "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" + }, + { + "name": "flag_black", + "unicode": "1F3F4", + "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" + }, + { + "name": "flag_bm", + "unicode": "1F1E7-1F1F2", + "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" + }, + { + "name": "flag_bn", + "unicode": "1F1E7-1F1F3", + "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" + }, + { + "name": "flag_bo", + "unicode": "1F1E7-1F1F4", + "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" + }, + { + "name": "flag_bq", + "unicode": "1F1E7-1F1F6", + "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" + }, + { + "name": "flag_br", + "unicode": "1F1E7-1F1F7", + "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" + }, + { + "name": "flag_bs", + "unicode": "1F1E7-1F1F8", + "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" + }, + { + "name": "flag_bt", + "unicode": "1F1E7-1F1F9", + "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" + }, + { + "name": "flag_bv", + "unicode": "1F1E7-1F1FB", + "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" + }, + { + "name": "flag_bw", + "unicode": "1F1E7-1F1FC", + "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" + }, + { + "name": "flag_by", + "unicode": "1F1E7-1F1FE", + "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" + }, + { + "name": "flag_bz", + "unicode": "1F1E7-1F1FF", + "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" + }, + { + "name": "flag_ca", + "unicode": "1F1E8-1F1E6", + "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" + }, + { + "name": "flag_cc", + "unicode": "1F1E8-1F1E8", + "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" + }, + { + "name": "flag_cd", + "unicode": "1F1E8-1F1E9", + "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" + }, + { + "name": "flag_cf", + "unicode": "1F1E8-1F1EB", + "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" + }, + { + "name": "flag_cg", + "unicode": "1F1E8-1F1EC", + "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" + }, + { + "name": "flag_ch", + "unicode": "1F1E8-1F1ED", + "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" + }, + { + "name": "flag_ci", + "unicode": "1F1E8-1F1EE", + "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" + }, + { + "name": "flag_ck", + "unicode": "1F1E8-1F1F0", + "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" + }, + { + "name": "flag_cl", + "unicode": "1F1E8-1F1F1", + "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" + }, + { + "name": "flag_cm", + "unicode": "1F1E8-1F1F2", + "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" + }, + { + "name": "flag_cn", + "unicode": "1F1E8-1F1F3", + "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" + }, + { + "name": "flag_co", + "unicode": "1F1E8-1F1F4", + "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" + }, + { + "name": "flag_cp", + "unicode": "1F1E8-1F1F5", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_cr", + "unicode": "1F1E8-1F1F7", + "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" + }, + { + "name": "flag_cu", + "unicode": "1F1E8-1F1FA", + "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" + }, + { + "name": "flag_cv", + "unicode": "1F1E8-1F1FB", + "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" + }, + { + "name": "flag_cw", + "unicode": "1F1E8-1F1FC", + "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" + }, + { + "name": "flag_cx", + "unicode": "1F1E8-1F1FD", + "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" + }, + { + "name": "flag_cy", + "unicode": "1F1E8-1F1FE", + "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" + }, + { + "name": "flag_cz", + "unicode": "1F1E8-1F1FF", + "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" + }, + { + "name": "flag_de", + "unicode": "1F1E9-1F1EA", + "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" + }, + { + "name": "flag_dg", + "unicode": "1F1E9-1F1EC", + "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" + }, + { + "name": "flag_dj", + "unicode": "1F1E9-1F1EF", + "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" + }, + { + "name": "flag_dk", + "unicode": "1F1E9-1F1F0", + "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" + }, + { + "name": "flag_dm", + "unicode": "1F1E9-1F1F2", + "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" + }, + { + "name": "flag_do", + "unicode": "1F1E9-1F1F4", + "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" + }, + { + "name": "flag_dz", + "unicode": "1F1E9-1F1FF", + "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" + }, + { + "name": "flag_ea", + "unicode": "1F1EA-1F1E6", + "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" + }, + { + "name": "flag_ec", + "unicode": "1F1EA-1F1E8", + "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" + }, + { + "name": "flag_ee", + "unicode": "1F1EA-1F1EA", + "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" + }, + { + "name": "flag_eg", + "unicode": "1F1EA-1F1EC", + "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" + }, + { + "name": "flag_eh", + "unicode": "1F1EA-1F1ED", + "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" + }, + { + "name": "flag_er", + "unicode": "1F1EA-1F1F7", + "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" + }, + { + "name": "flag_es", + "unicode": "1F1EA-1F1F8", + "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" + }, + { + "name": "flag_et", + "unicode": "1F1EA-1F1F9", + "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" + }, + { + "name": "flag_eu", + "unicode": "1F1EA-1F1FA", + "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" + }, + { + "name": "flag_fi", + "unicode": "1F1EB-1F1EE", + "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" + }, + { + "name": "flag_fj", + "unicode": "1F1EB-1F1EF", + "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" + }, + { + "name": "flag_fk", + "unicode": "1F1EB-1F1F0", + "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" + }, + { + "name": "flag_fm", + "unicode": "1F1EB-1F1F2", + "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" + }, + { + "name": "flag_fo", + "unicode": "1F1EB-1F1F4", + "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" + }, + { + "name": "flag_fr", + "unicode": "1F1EB-1F1F7", + "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" + }, + { + "name": "flag_ga", + "unicode": "1F1EC-1F1E6", + "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" + }, + { + "name": "flag_gb", + "unicode": "1F1EC-1F1E7", + "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" + }, + { + "name": "flag_gd", + "unicode": "1F1EC-1F1E9", + "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" + }, + { + "name": "flag_ge", + "unicode": "1F1EC-1F1EA", + "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" + }, + { + "name": "flag_gf", + "unicode": "1F1EC-1F1EB", + "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" + }, + { + "name": "flag_gg", + "unicode": "1F1EC-1F1EC", + "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" + }, + { + "name": "flag_gh", + "unicode": "1F1EC-1F1ED", + "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" + }, + { + "name": "flag_gi", + "unicode": "1F1EC-1F1EE", + "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" + }, + { + "name": "flag_gl", + "unicode": "1F1EC-1F1F1", + "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" + }, + { + "name": "flag_gm", + "unicode": "1F1EC-1F1F2", + "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" + }, + { + "name": "flag_gn", + "unicode": "1F1EC-1F1F3", + "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" + }, + { + "name": "flag_gp", + "unicode": "1F1EC-1F1F5", + "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" + }, + { + "name": "flag_gq", + "unicode": "1F1EC-1F1F6", + "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" + }, + { + "name": "flag_gr", + "unicode": "1F1EC-1F1F7", + "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" + }, + { + "name": "flag_gs", + "unicode": "1F1EC-1F1F8", + "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" + }, + { + "name": "flag_gt", + "unicode": "1F1EC-1F1F9", + "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" + }, + { + "name": "flag_gu", + "unicode": "1F1EC-1F1FA", + "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" + }, + { + "name": "flag_gw", + "unicode": "1F1EC-1F1FC", + "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" + }, + { + "name": "flag_gy", + "unicode": "1F1EC-1F1FE", + "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" + }, + { + "name": "flag_hk", + "unicode": "1F1ED-1F1F0", + "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" + }, + { + "name": "flag_hm", + "unicode": "1F1ED-1F1F2", + "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" + }, + { + "name": "flag_hn", + "unicode": "1F1ED-1F1F3", + "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" + }, + { + "name": "flag_hr", + "unicode": "1F1ED-1F1F7", + "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" + }, + { + "name": "flag_ht", + "unicode": "1F1ED-1F1F9", + "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" + }, + { + "name": "flag_hu", + "unicode": "1F1ED-1F1FA", + "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" + }, + { + "name": "flag_ic", + "unicode": "1F1EE-1F1E8", + "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" + }, + { + "name": "flag_id", + "unicode": "1F1EE-1F1E9", + "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" + }, + { + "name": "flag_ie", + "unicode": "1F1EE-1F1EA", + "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" + }, + { + "name": "flag_il", + "unicode": "1F1EE-1F1F1", + "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" + }, + { + "name": "flag_im", + "unicode": "1F1EE-1F1F2", + "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" + }, + { + "name": "flag_in", + "unicode": "1F1EE-1F1F3", + "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" + }, + { + "name": "flag_io", + "unicode": "1F1EE-1F1F4", + "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" + }, + { + "name": "flag_iq", + "unicode": "1F1EE-1F1F6", + "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" + }, + { + "name": "flag_ir", + "unicode": "1F1EE-1F1F7", + "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" + }, + { + "name": "flag_is", + "unicode": "1F1EE-1F1F8", + "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" + }, + { + "name": "flag_it", + "unicode": "1F1EE-1F1F9", + "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" + }, + { + "name": "flag_je", + "unicode": "1F1EF-1F1EA", + "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" + }, + { + "name": "flag_jm", + "unicode": "1F1EF-1F1F2", + "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" + }, + { + "name": "flag_jo", + "unicode": "1F1EF-1F1F4", + "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" + }, + { + "name": "flag_jp", + "unicode": "1F1EF-1F1F5", + "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" + }, + { + "name": "flag_ke", + "unicode": "1F1F0-1F1EA", + "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" + }, + { + "name": "flag_kg", + "unicode": "1F1F0-1F1EC", + "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" + }, + { + "name": "flag_kh", + "unicode": "1F1F0-1F1ED", + "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" + }, + { + "name": "flag_ki", + "unicode": "1F1F0-1F1EE", + "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" + }, + { + "name": "flag_km", + "unicode": "1F1F0-1F1F2", + "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" + }, + { + "name": "flag_kn", + "unicode": "1F1F0-1F1F3", + "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" + }, + { + "name": "flag_kp", + "unicode": "1F1F0-1F1F5", + "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" + }, + { + "name": "flag_kr", + "unicode": "1F1F0-1F1F7", + "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" + }, + { + "name": "flag_kw", + "unicode": "1F1F0-1F1FC", + "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" + }, + { + "name": "flag_ky", + "unicode": "1F1F0-1F1FE", + "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" + }, + { + "name": "flag_kz", + "unicode": "1F1F0-1F1FF", + "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" + }, + { + "name": "flag_la", + "unicode": "1F1F1-1F1E6", + "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" + }, + { + "name": "flag_lb", + "unicode": "1F1F1-1F1E7", + "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" + }, + { + "name": "flag_lc", + "unicode": "1F1F1-1F1E8", + "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" + }, + { + "name": "flag_li", + "unicode": "1F1F1-1F1EE", + "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" + }, + { + "name": "flag_lk", + "unicode": "1F1F1-1F1F0", + "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" + }, + { + "name": "flag_lr", + "unicode": "1F1F1-1F1F7", + "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" + }, + { + "name": "flag_ls", + "unicode": "1F1F1-1F1F8", + "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" + }, + { + "name": "flag_lt", + "unicode": "1F1F1-1F1F9", + "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" + }, + { + "name": "flag_lu", + "unicode": "1F1F1-1F1FA", + "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" + }, + { + "name": "flag_lv", + "unicode": "1F1F1-1F1FB", + "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" + }, + { + "name": "flag_ly", + "unicode": "1F1F1-1F1FE", + "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" + }, + { + "name": "flag_ma", + "unicode": "1F1F2-1F1E6", + "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" + }, + { + "name": "flag_mc", + "unicode": "1F1F2-1F1E8", + "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" + }, + { + "name": "flag_md", + "unicode": "1F1F2-1F1E9", + "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" + }, + { + "name": "flag_me", + "unicode": "1F1F2-1F1EA", + "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" + }, + { + "name": "flag_mf", + "unicode": "1F1F2-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_mg", + "unicode": "1F1F2-1F1EC", + "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" + }, + { + "name": "flag_mh", + "unicode": "1F1F2-1F1ED", + "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" + }, + { + "name": "flag_mk", + "unicode": "1F1F2-1F1F0", + "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" + }, + { + "name": "flag_ml", + "unicode": "1F1F2-1F1F1", + "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" + }, + { + "name": "flag_mm", + "unicode": "1F1F2-1F1F2", + "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" + }, + { + "name": "flag_mn", + "unicode": "1F1F2-1F1F3", + "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" + }, + { + "name": "flag_mo", + "unicode": "1F1F2-1F1F4", + "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" + }, + { + "name": "flag_mp", + "unicode": "1F1F2-1F1F5", + "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" + }, + { + "name": "flag_mq", + "unicode": "1F1F2-1F1F6", + "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" + }, + { + "name": "flag_mr", + "unicode": "1F1F2-1F1F7", + "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" + }, + { + "name": "flag_ms", + "unicode": "1F1F2-1F1F8", + "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" + }, + { + "name": "flag_mt", + "unicode": "1F1F2-1F1F9", + "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" + }, + { + "name": "flag_mu", + "unicode": "1F1F2-1F1FA", + "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" + }, + { + "name": "flag_mv", + "unicode": "1F1F2-1F1FB", + "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" + }, + { + "name": "flag_mw", + "unicode": "1F1F2-1F1FC", + "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" + }, + { + "name": "flag_mx", + "unicode": "1F1F2-1F1FD", + "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" + }, + { + "name": "flag_my", + "unicode": "1F1F2-1F1FE", + "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" + }, + { + "name": "flag_mz", + "unicode": "1F1F2-1F1FF", + "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" + }, + { + "name": "flag_na", + "unicode": "1F1F3-1F1E6", + "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" + }, + { + "name": "flag_nc", + "unicode": "1F1F3-1F1E8", + "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" + }, + { + "name": "flag_ne", + "unicode": "1F1F3-1F1EA", + "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" + }, + { + "name": "flag_nf", + "unicode": "1F1F3-1F1EB", + "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" + }, + { + "name": "flag_ng", + "unicode": "1F1F3-1F1EC", + "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" + }, + { + "name": "flag_ni", + "unicode": "1F1F3-1F1EE", + "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" + }, + { + "name": "flag_nl", + "unicode": "1F1F3-1F1F1", + "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" + }, + { + "name": "flag_no", + "unicode": "1F1F3-1F1F4", + "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" + }, + { + "name": "flag_np", + "unicode": "1F1F3-1F1F5", + "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" + }, + { + "name": "flag_nr", + "unicode": "1F1F3-1F1F7", + "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" + }, + { + "name": "flag_nu", + "unicode": "1F1F3-1F1FA", + "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" + }, + { + "name": "flag_nz", + "unicode": "1F1F3-1F1FF", + "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" + }, + { + "name": "flag_om", + "unicode": "1F1F4-1F1F2", + "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" + }, + { + "name": "flag_pa", + "unicode": "1F1F5-1F1E6", + "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" + }, + { + "name": "flag_pe", + "unicode": "1F1F5-1F1EA", + "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" + }, + { + "name": "flag_pf", + "unicode": "1F1F5-1F1EB", + "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" + }, + { + "name": "flag_pg", + "unicode": "1F1F5-1F1EC", + "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" + }, + { + "name": "flag_ph", + "unicode": "1F1F5-1F1ED", + "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" + }, + { + "name": "flag_pk", + "unicode": "1F1F5-1F1F0", + "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" + }, + { + "name": "flag_pl", + "unicode": "1F1F5-1F1F1", + "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" + }, + { + "name": "flag_pm", + "unicode": "1F1F5-1F1F2", + "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" + }, + { + "name": "flag_pn", + "unicode": "1F1F5-1F1F3", + "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" + }, + { + "name": "flag_pr", + "unicode": "1F1F5-1F1F7", + "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" + }, + { + "name": "flag_ps", + "unicode": "1F1F5-1F1F8", + "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" + }, + { + "name": "flag_pt", + "unicode": "1F1F5-1F1F9", + "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" + }, + { + "name": "flag_pw", + "unicode": "1F1F5-1F1FC", + "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" + }, + { + "name": "flag_py", + "unicode": "1F1F5-1F1FE", + "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" + }, + { + "name": "flag_qa", + "unicode": "1F1F6-1F1E6", + "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" + }, + { + "name": "flag_re", + "unicode": "1F1F7-1F1EA", + "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" + }, + { + "name": "flag_ro", + "unicode": "1F1F7-1F1F4", + "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" + }, + { + "name": "flag_rs", + "unicode": "1F1F7-1F1F8", + "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" + }, + { + "name": "flag_ru", + "unicode": "1F1F7-1F1FA", + "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" + }, + { + "name": "flag_rw", + "unicode": "1F1F7-1F1FC", + "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" + }, + { + "name": "flag_sa", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, + { + "name": "flag_sb", + "unicode": "1F1F8-1F1E7", + "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" + }, + { + "name": "flag_sc", + "unicode": "1F1F8-1F1E8", + "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" + }, + { + "name": "flag_sd", + "unicode": "1F1F8-1F1E9", + "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" + }, + { + "name": "flag_se", + "unicode": "1F1F8-1F1EA", + "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" + }, + { + "name": "flag_sg", + "unicode": "1F1F8-1F1EC", + "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" + }, + { + "name": "flag_sh", + "unicode": "1F1F8-1F1ED", + "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" + }, + { + "name": "flag_si", + "unicode": "1F1F8-1F1EE", + "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" + }, + { + "name": "flag_sj", + "unicode": "1F1F8-1F1EF", + "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" + }, + { + "name": "flag_sk", + "unicode": "1F1F8-1F1F0", + "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" + }, + { + "name": "flag_sl", + "unicode": "1F1F8-1F1F1", + "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" + }, + { + "name": "flag_sm", + "unicode": "1F1F8-1F1F2", + "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" + }, + { + "name": "flag_sn", + "unicode": "1F1F8-1F1F3", + "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" + }, + { + "name": "flag_so", + "unicode": "1F1F8-1F1F4", + "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" + }, + { + "name": "flag_sr", + "unicode": "1F1F8-1F1F7", + "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" + }, + { + "name": "flag_ss", + "unicode": "1F1F8-1F1F8", + "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" + }, + { + "name": "flag_st", + "unicode": "1F1F8-1F1F9", + "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" + }, + { + "name": "flag_sv", + "unicode": "1F1F8-1F1FB", + "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" + }, + { + "name": "flag_sx", + "unicode": "1F1F8-1F1FD", + "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" + }, + { + "name": "flag_sy", + "unicode": "1F1F8-1F1FE", + "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" + }, + { + "name": "flag_sz", + "unicode": "1F1F8-1F1FF", + "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" + }, + { + "name": "flag_ta", + "unicode": "1F1F9-1F1E6", + "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" + }, + { + "name": "flag_tc", + "unicode": "1F1F9-1F1E8", + "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" + }, + { + "name": "flag_td", + "unicode": "1F1F9-1F1E9", + "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" + }, + { + "name": "flag_tf", + "unicode": "1F1F9-1F1EB", + "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" + }, + { + "name": "flag_tg", + "unicode": "1F1F9-1F1EC", + "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" + }, + { + "name": "flag_th", + "unicode": "1F1F9-1F1ED", + "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" + }, + { + "name": "flag_tj", + "unicode": "1F1F9-1F1EF", + "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" + }, + { + "name": "flag_tk", + "unicode": "1F1F9-1F1F0", + "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" + }, + { + "name": "flag_tl", + "unicode": "1F1F9-1F1F1", + "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" + }, + { + "name": "flag_tm", + "unicode": "1F1F9-1F1F2", + "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" + }, + { + "name": "flag_tn", + "unicode": "1F1F9-1F1F3", + "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" + }, + { + "name": "flag_to", + "unicode": "1F1F9-1F1F4", + "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" + }, + { + "name": "flag_tr", + "unicode": "1F1F9-1F1F7", + "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" + }, + { + "name": "flag_tt", + "unicode": "1F1F9-1F1F9", + "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" + }, + { + "name": "flag_tv", + "unicode": "1F1F9-1F1FB", + "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" + }, + { + "name": "flag_tw", + "unicode": "1F1F9-1F1FC", + "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" + }, + { + "name": "flag_tz", + "unicode": "1F1F9-1F1FF", + "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" + }, + { + "name": "flag_ua", + "unicode": "1F1FA-1F1E6", + "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" + }, + { + "name": "flag_ug", + "unicode": "1F1FA-1F1EC", + "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" + }, + { + "name": "flag_um", + "unicode": "1F1FA-1F1F2", + "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" + }, + { + "name": "flag_us", + "unicode": "1F1FA-1F1F8", + "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" + }, + { + "name": "flag_uy", + "unicode": "1F1FA-1F1FE", + "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" + }, + { + "name": "flag_uz", + "unicode": "1F1FA-1F1FF", + "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" + }, + { + "name": "flag_va", + "unicode": "1F1FB-1F1E6", + "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" + }, + { + "name": "flag_vc", + "unicode": "1F1FB-1F1E8", + "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" + }, + { + "name": "flag_ve", + "unicode": "1F1FB-1F1EA", + "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" + }, + { + "name": "flag_vg", + "unicode": "1F1FB-1F1EC", + "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" + }, + { + "name": "flag_vi", + "unicode": "1F1FB-1F1EE", + "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" + }, + { + "name": "flag_vn", + "unicode": "1F1FB-1F1F3", + "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" + }, + { + "name": "flag_vu", + "unicode": "1F1FB-1F1FA", + "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" + }, + { + "name": "flag_wf", + "unicode": "1F1FC-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_white", + "unicode": "1F3F3", + "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" + }, + { + "name": "flag_ws", + "unicode": "1F1FC-1F1F8", + "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" + }, + { + "name": "flag_xk", + "unicode": "1F1FD-1F1F0", + "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" + }, + { + "name": "flag_ye", + "unicode": "1F1FE-1F1EA", + "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" + }, + { + "name": "flag_yt", + "unicode": "1F1FE-1F1F9", + "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" + }, + { + "name": "flag_za", + "unicode": "1F1FF-1F1E6", + "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" + }, + { + "name": "flag_zm", + "unicode": "1F1FF-1F1F2", + "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" + }, + { + "name": "flag_zw", + "unicode": "1F1FF-1F1FC", + "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" + }, + { + "name": "flags", + "unicode": "1F38F", + "digest": "c3f4a66786e524a5562919afcba9486113091ed205f1342e91d2f6439845ad61" + }, + { + "name": "flashlight", + "unicode": "1F526", + "digest": "5f641b8fd1c7f1dcd43ec3b1ef78d14ef9929d723789c5567aca8b95d3d39803" + }, + { + "name": "fleur-de-lis", + "unicode": "269C", + "digest": "d6ddeeea355ed55103b7fc65ac1ee0dbaa79d01e0d136b265363a6b92284c073" + }, + { + "name": "flip_phone", + "unicode": "1F581", + "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" + }, + { + "name": "floppy_black", + "unicode": "1F5AA", + "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" + }, + { + "name": "floppy_disk", + "unicode": "1F4BE", + "digest": "e987961ca516032a90942ef6c398836f2da68a5981714bd172acfe7b0e369d0a" + }, + { + "name": "floppy_white", + "unicode": "1F5AB", + "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" + }, + { + "name": "flower_playing_cards", + "unicode": "1F3B4", + "digest": "451f361050b96ba9ed8dc5b64c8a90c1316fd9b83fb818152881a54e100eea6c" + }, + { + "name": "flushed", + "unicode": "1F633", + "digest": "39cf51f9dec2a910c66ecd39a7bd616fea09d67e81801e57e84f03ed1e917750" + }, + { + "name": "fog", + "unicode": "1F32B", + "digest": "da6fdb9b682ed9a3368adcd7531f1a29e22755a620e3cca163fc3f33a6a78107" + }, + { + "name": "foggy", + "unicode": "1F301", + "digest": "b599f3178db289c6e30017f3f0a9d30b00a75417057c7a10c0c9eedac78edbf1" + }, + { + "name": "folder", + "unicode": "1F5C0", + "digest": "8932141321911032ce8469ba85fe309b78384545c3b9946978b383670b956644" + }, + { + "name": "folder_open", + "unicode": "1F5C1", + "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" + }, + { + "name": "football", + "unicode": "1F3C8", + "digest": "834fe5f431d6aa8ef1186aa79e71f813393535d273483b6af4cc4bdb8380e5b4" + }, + { + "name": "footprints", + "unicode": "1F463", + "digest": "60dc938f6769ea21b05b5afcc481d3ddacf1f565e04f33310b271d5422e7ceb9" + }, + { + "name": "fork_and_knife", + "unicode": "1F374", + "digest": "7e07c9dc555d172fa2eaa41cefd8d46d9624be0137aff196dd003a8a82610ec3" + }, + { + "name": "fork_knife_plate", + "unicode": "1F37D", + "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" + }, + { + "name": "fountain", + "unicode": "26F2", + "digest": "0acdca5e8f6d745a8d582d96012ec8fc55b9f5447e657ebfd998a4e332d99322" + }, + { + "name": "four", + "unicode": "0034-20E3", + "digest": "36bd4ea6e2ae689835a79f8e60466eccd62fce7e91e84ed768cffd87dac628dd" + }, + { + "name": "four_leaf_clover", + "unicode": "1F340", + "digest": "12ee2343df25bbd9077fdc12314c1edb51c0cdb556af7e22590e8a578ef57f17" + }, + { + "name": "frame_photo", + "unicode": "1F5BC", + "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" + }, + { + "name": "frame_tiles", + "unicode": "1F5BD", + "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" + }, + { + "name": "frame_x", + "unicode": "1F5BE", + "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" + }, + { + "name": "free", + "unicode": "1F193", + "digest": "c1d9172a656717f78d941303c5da8790c6cd9827838d8f7dc3719afb53bcab80" + }, + { + "name": "fried_shrimp", + "unicode": "1F364", + "digest": "c0c19e95f2c38f6cf870920bf3c2d4d69c36ea6e7dc9a5c45c3e8b285269d40a" + }, + { + "name": "fries", + "unicode": "1F35F", + "digest": "0f546534684de29d319cbcbab4162acb321c4f8f3202fe17d69e1894ab7c8195" + }, + { + "name": "frog", + "unicode": "1F438", + "digest": "6a417757fa6ee39e7a277cbd53c690ff88af0b1d76728d56f9bc645cb628aeb7" + }, + { + "name": "frowning", + "unicode": "1F626", + "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" + }, + { + "name": "frowning2", + "unicode": "2639", + "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" + }, + { + "name": "fuelpump", + "unicode": "26FD", + "digest": "9cbb2646c93b255bd3de87dc01aa1193ab96e39a3013975d250472ab8aae61d6" + }, + { + "name": "full_moon", + "unicode": "1F315", + "digest": "0b4f08ef2089397ead034b444a60e6e9810073454581b52a46b2369e3b9cd5f9" + }, + { + "name": "full_moon_with_face", + "unicode": "1F31D", + "digest": "a371cb9e1f28a7db739dd058234642a2e333dff4b6df9882df85a6d984e4b5e8" + }, + { + "name": "game_die", + "unicode": "1F3B2", + "digest": "6584909a4348c350c04417421b63eace1245087f7d239051b30a0cd37fe929f9" + }, + { + "name": "gear", + "unicode": "2699", + "digest": "b0ff5fd007daa366a9eecb7422dbeb8a973e123a04267b88fef96c7453238294" + }, + { + "name": "gem", + "unicode": "1F48E", + "digest": "d75d854f35975e4e291c3b9fcaf8437467f6d7eb27b29e2d7c0f0038fc666fe2" + }, + { + "name": "gemini", + "unicode": "264A", + "digest": "392abe62872736a0bf92979a8c25a814985d0ff0a08dc7ab2a5c058aeda7e685" + }, + { + "name": "ghost", + "unicode": "1F47B", + "digest": "f084b14483476e2d07563840f8c33b46da9c17f791da07fde3acffeb77342947" + }, + { + "name": "gift", + "unicode": "1F381", + "digest": "c9a2ae6ea05c02e78e9567dcbd971701a2f869eb46c62d85cef23d0834388d8c" + }, + { + "name": "gift_heart", + "unicode": "1F49D", + "digest": "e0c5aacf1ce89117d86b148f10a02dc18fe0cd22a75fbf6f0f88f2fad3ca80fe" + }, + { + "name": "girl", + "unicode": "1F467", + "digest": "0758cbc4cbc7d72d6df8f66fc3a6b2b283c6634b053e59d61c6cac44cf8bffda" + }, + { + "name": "girl_tone1", + "unicode": "1F467-1F3FB", + "digest": "7afdece55cb64e8056e2202de8c17b66ddb616f224ac374ec9a160d06b3138cc" + }, + { + "name": "girl_tone2", + "unicode": "1F467-1F3FC", + "digest": "c160aa65fee70ad52930d01246ac9f282ff6abf1d93c5cc5b299fc257ee81db1" + }, + { + "name": "girl_tone3", + "unicode": "1F467-1F3FD", + "digest": "b8a5687cd637855a41b8c7dc686f0e69fda379875408cd269f1b330a805c72f4" + }, + { + "name": "girl_tone4", + "unicode": "1F467-1F3FE", + "digest": "a9cf743936b733634f323790a1abe3a410601b6841484baebea484b392f4e98e" + }, + { + "name": "girl_tone5", + "unicode": "1F467-1F3FF", + "digest": "c902170e67b81eee35eeefb6a5c62c6109cb423dcae88d4e036ddd50b240c072" + }, + { + "name": "girls_symbol", + "unicode": "1F6CA", + "digest": "2c55aee81defd7a1620ffeaad8d9bcc1835f19237c72c79633aec45671ddb9ff" + }, + { + "name": "globe_with_meridians", + "unicode": "1F310", + "digest": "945646de3d8f057760fe374494a253d9a6aa8a132309154b0a5bdbffb5b20c3f" + }, + { + "name": "goat", + "unicode": "1F410", + "digest": "f99cbc6755d119cb5c1dce08cabd20871f98d009bb773da4a146dae60476a235" + }, + { + "name": "golf", + "unicode": "26F3", + "digest": "74a7876d185f8ff6a6533e4db2e1eb787119b2f8d8b07c36d99ec3163fb48485" + }, + { + "name": "golfer", + "unicode": "1F3CC", + "digest": "6458295a5e4a6e4323c32a7f1f7182fb2d3918083839efc380d995860ce360b1" + }, + { + "name": "grapes", + "unicode": "1F347", + "digest": "7f6873d65180ab476f49d207ac2d1f7dbaf6c8b0b561d50b64325e192cf97a86" + }, + { + "name": "green_apple", + "unicode": "1F34F", + "digest": "effc3fe60f2ab704a034c794bfccfa023b41332f8f16ca44cc8ea41698f03873" + }, + { + "name": "green_book", + "unicode": "1F4D7", + "digest": "6652c4d2ccfa4a287a5d45007bd06cadc16d34b0a1ca4b6b13b46f976c8d8319" + }, + { + "name": "green_heart", + "unicode": "1F49A", + "digest": "f4bcb660a1d3cf3692238359d8b9de9a725a9af81f166253e487d61b8ccf9d86" + }, + { + "name": "grey_exclamation", + "unicode": "2755", + "digest": "ac8cdab7496d133e7bc9475f2fdb0cf59b3ccba20f2f156c8b693e72b5948078" + }, + { + "name": "grey_question", + "unicode": "2754", + "digest": "c173e1b2a16ab62b0abd7a58deb7a6df709b072d30d001627b92d0123a3a3e4a" + }, + { + "name": "grimacing", + "unicode": "1F62C", + "digest": "8c54b73f5d2c1c6347e2c0ab01616519e0fb34490daa9c36664d442c6851c57e" + }, + { + "name": "grin", + "unicode": "1F601", + "digest": "916eabdabd8b7ca698e638bbbd14affff97464ec11a3b59c0cb96cd7705600d8" + }, + { + "name": "grinning", + "unicode": "1F600", + "digest": "3d8665c03f272ca3063e96145989926355a7ac315ed1a032d30fcefa6f0c3923" + }, + { + "name": "guardsman", + "unicode": "1F482", + "digest": "ebbd29fa138005232d64fca4a8ec015d097fa14e6ded57b35ac257b4570b3c36" + }, + { + "name": "guardsman_tone1", + "unicode": "1F482-1F3FB", + "digest": "b6082c8fee5dbc3ce2540f3939d5e344b5366c9f07827345facaba438e7017ff" + }, + { + "name": "guardsman_tone2", + "unicode": "1F482-1F3FC", + "digest": "2b813afe1c2bbdaf9a47493393a0e6c400a16e453ed25a9a9c0035197927b56e" + }, + { + "name": "guardsman_tone3", + "unicode": "1F482-1F3FD", + "digest": "49b2fa1ad0bc50a5ef6d73fb140aa1876506b9ebb9d45782ccb8dbb6818f8dde" + }, + { + "name": "guardsman_tone4", + "unicode": "1F482-1F3FE", + "digest": "a584e1e3a8ad7be4871a6bdb7996d4f649abeaa77eb5d1cae998058d8b23ca0f" + }, + { + "name": "guardsman_tone5", + "unicode": "1F482-1F3FF", + "digest": "e853b67ee13fda99e98f47083529ca80c404df1b19352c78b9c69850eb8f2c76" + }, + { + "name": "guitar", + "unicode": "1F3B8", + "digest": "8c041b961649cc5917f56f2fb543f9a5280724647ed2fc67bc94a05eff9da805" + }, + { + "name": "gun", + "unicode": "1F52B", + "digest": "d7f5aa657cc0ba04d878511820632b89c305a9b4d6c4a4b90ff691dad9906607" + }, + { + "name": "haircut", + "unicode": "1F487", + "digest": "369dbab1b138c31d3eca04c950fdab4ec9f085272268c241f100d44e7b0f229e" + }, + { + "name": "haircut_tone1", + "unicode": "1F487-1F3FB", + "digest": "c56f32d7c1d8a92d22429133f87f31a159818939cfdc570cb48b6d243cc58cf2" + }, + { + "name": "haircut_tone2", + "unicode": "1F487-1F3FC", + "digest": "e916e040ffb8e869e930d1256343af2ad2bbaa683f01a11564d0777019944bec" + }, + { + "name": "haircut_tone3", + "unicode": "1F487-1F3FD", + "digest": "f07cdfbea964ac42a9a050f832107ef0f2fa8115b27689f93d1be954de07b7c1" + }, + { + "name": "haircut_tone4", + "unicode": "1F487-1F3FE", + "digest": "32ec7f5e999f7c43676768c8320ffaa346c713d340a94b948b1f564b345a2d11" + }, + { + "name": "haircut_tone5", + "unicode": "1F487-1F3FF", + "digest": "5aad997d09e7975700927906d41a10bae774356ccddbe5197980bde670272262" + }, + { + "name": "hamburger", + "unicode": "1F354", + "digest": "24ebae9a69cf283ab198499cb38d0cdcd82bac74c8e8d1e769ad78eb320a4294" + }, + { + "name": "hammer", + "unicode": "1F528", + "digest": "a43a66b0efdc4cd2c84fd0ccc2cb8e9ede1f89c5d62eefa6ae521d3aed9d81b3" + }, + { + "name": "hammer_pick", + "unicode": "2692", + "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" + }, + { + "name": "hamster", + "unicode": "1F439", + "digest": "f47da088ff5792532a382b6e3a47d2dd7c5e6fc19abd5ff6c5ba3ce420b4192e" + }, + { + "name": "hand_splayed", + "unicode": "1F590", + "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" + }, + { + "name": "hand_splayed_reverse", + "unicode": "1F591", + "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" + }, + { + "name": "hand_splayed_tone1", + "unicode": "1F590-1F3FB", + "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" + }, + { + "name": "hand_splayed_tone2", + "unicode": "1F590-1F3FC", + "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" + }, + { + "name": "hand_splayed_tone3", + "unicode": "1F590-1F3FD", + "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" + }, + { + "name": "hand_splayed_tone4", + "unicode": "1F590-1F3FE", + "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" + }, + { + "name": "hand_splayed_tone5", + "unicode": "1F590-1F3FF", + "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" + }, + { + "name": "hand_victory", + "unicode": "1F594", + "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" + }, + { + "name": "handbag", + "unicode": "1F45C", + "digest": "f1e2822c67f659b52c76821dd9db001332215a8566fc1846c89b6019c9758038" + }, + { + "name": "hard_disk", + "unicode": "1F5B4", + "digest": "df8549d4281f5ae70fb6792a02c078e651764b0276aa43b7407236bd38fc21b4" + }, + { + "name": "hash", + "unicode": "0023-20E3", + "digest": "5bd5c7180485fa71accdec5378bdc196ce0602f594f91e4eadc1e7514d5d0f90" + }, + { + "name": "hatched_chick", + "unicode": "1F425", + "digest": "7995c3eb503a8b9662694eba80a9b551216473a31928091e35cd6ebc21cee083" + }, + { + "name": "hatching_chick", + "unicode": "1F423", + "digest": "22905b42fa65dbc9aad8940d2db13691cacc62014f54e0960978ee0002178e1b" + }, + { + "name": "head_bandage", + "unicode": "1F915", + "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" + }, + { + "name": "headphones", + "unicode": "1F3A7", + "digest": "219da138032c01c97a94f02b211049418191a3beb3d159804b9033f5916fd3c8" + }, + { + "name": "hear_no_evil", + "unicode": "1F649", + "digest": "8120060238eaca645809dd113862a144f10395afcb3837ab60c0f04009b49a2f" + }, + { + "name": "heart", + "unicode": "2764", + "digest": "a646a25a36f431cadc7e56afd1a4d1b7cbae5292a25d7783bd31462d0d3d719b" + }, + { + "name": "heart_decoration", + "unicode": "1F49F", + "digest": "a83989669347c98cb74065d4f0befedbc37f82c91214e773245cb6810ab359b4" + }, + { + "name": "heart_exclamation", + "unicode": "2763", + "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" + }, + { + "name": "heart_eyes", + "unicode": "1F60D", + "digest": "335ea73efca4824e623a5a51ccdb494c8b1f5f10b4139b39b250a2a771876b0d" + }, + { + "name": "heart_eyes_cat", + "unicode": "1F63B", + "digest": "9346b85afb80f7b498cc255426ea15a287f81d8fb3c26dab61337635f439d3ce" + }, + { + "name": "heart_tip", + "unicode": "1F394", + "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" + }, + { + "name": "heartbeat", + "unicode": "1F493", + "digest": "cd6921ce55c155873220a09416d695c4bcca1556007066d6d185e93d6561e825" + }, + { + "name": "heartpulse", + "unicode": "1F497", + "digest": "f869357b9e678d9671ec38c569fc88efec48006c159b69297277cee795dc4dc9" + }, + { + "name": "hearts", + "unicode": "2665", + "digest": "17dc9b2941561f58ca0f04d0754b1eff3490b63b17241580b3d4aa4638fa85e8" + }, + { + "name": "heavy_check_mark", + "unicode": "2714", + "digest": "b5fa24f6e0f1dcbd6278e9125154522f2efd79e6dd0836ccb792a1f3aeeff2b2" + }, + { + "name": "heavy_division_sign", + "unicode": "2797", + "digest": "59a6983d788f347c64eecb3df6f7d3b36779d92df6cc811820993ff9e18d77e1" + }, + { + "name": "heavy_dollar_sign", + "unicode": "1F4B2", + "digest": "d2e89c54b3fdeda4d1fd4d29454b69dcf750181110894e6e71a40df99c95bfe8" + }, + { + "name": "heavy_minus_sign", + "unicode": "2796", + "digest": "dd5ab3722fe49cfdbc5e1fbab5b342dc960de7b412d4fba59d66e06ce3dc3bcd" + }, + { + "name": "heavy_multiplication_x", + "unicode": "2716", + "digest": "7d77742f91377785675802f40bd8dde9bd1feeb513735760a58ea9bee8a65d44" + }, + { + "name": "heavy_plus_sign", + "unicode": "2795", + "digest": "9aa9dcdbba120a4b485c21f67589609b789c6e3edf08479ff8268fa0db973ad7" + }, + { + "name": "helicopter", + "unicode": "1F681", + "digest": "b259ea8d2bdca36766075894da650b1d3ff4c8602259cd0d30cb8214cd585340" + }, + { + "name": "helmet_with_cross", + "unicode": "26D1", + "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" + }, + { + "name": "herb", + "unicode": "1F33F", + "digest": "3c452106b1966f643751bf161fa7d1762a33e6fff381b2109bb53b55c4fdd129" + }, + { + "name": "hibiscus", + "unicode": "1F33A", + "digest": "268963a1f3cdad9050d9ae31c558e010f33812e3b09bbf9088ba876c033d8b2f" + }, + { + "name": "high_brightness", + "unicode": "1F506", + "digest": "d607f6269d95dd16c2a7932e49ac09e44f4c19e0a34f6c0f21ecb945a2316361" + }, + { + "name": "high_heel", + "unicode": "1F460", + "digest": "5c320d5954bf4f4dacacddd562c1598ab101731077a6656ac5d2bfd41405483e" + }, + { + "name": "hockey", + "unicode": "1F3D2", + "digest": "008904c1b8db139215492a6d96c09f2c3eeda769f858a9bbae13f8c54d439d0e" + }, + { + "name": "hole", + "unicode": "1F573", + "digest": "36bbafa5e89b1410ec74919aaf60b09ac3525a421cb5b475b9bb2f20357db8de" + }, + { + "name": "homes", + "unicode": "1F3D8", + "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" + }, + { + "name": "honey_pot", + "unicode": "1F36F", + "digest": "94cb1624491076b5cb145e7a309f91a7be3d4c0bed712af6a51d641eb73edee7" + }, + { + "name": "horse", + "unicode": "1F434", + "digest": "624ad9dc9ed7af3f6e1a2f9d4ed483702ae64ed5fbcf5e9918af6bfef24e76f9" + }, + { + "name": "horse_racing", + "unicode": "1F3C7", + "digest": "c2702b7225e9839a789dda7c43f0cc86dced2b4d5d3787116106396633362de6" + }, + { + "name": "horse_racing_tone1", + "unicode": "1F3C7-1F3FB", + "digest": "a7ed284f9d5cd8a4fe4a09cb91c3f99e5db99c7e31c5f525c14de97b06857d92" + }, + { + "name": "horse_racing_tone2", + "unicode": "1F3C7-1F3FC", + "digest": "20b4d61b21ee6ba860b029f0ad0e38f5ecb6dd2c774f7b7801fba07ed33f96be" + }, + { + "name": "horse_racing_tone3", + "unicode": "1F3C7-1F3FD", + "digest": "dd65f7bb96ee44507d26e524202d567d2d7679d571245299a2a84f68bd5def4c" + }, + { + "name": "horse_racing_tone4", + "unicode": "1F3C7-1F3FE", + "digest": "36afaad218a4c820b19c7c9bbbc187119d47b41273d8f48ab14cc3e32dd7c21f" + }, + { + "name": "horse_racing_tone5", + "unicode": "1F3C7-1F3FF", + "digest": "2e0efd501a4471428533ce7909972a49ff045369261c27e4abb97ee2aede2f47" + }, + { + "name": "hospital", + "unicode": "1F3E5", + "digest": "df5c774fa36b2601e6960a7b81cdfac71c1d2d71f04dea88068d1c9043e313bb" + }, + { + "name": "hot_pepper", + "unicode": "1F336", + "digest": "62e4dade3c793f6d83530bd1f60f3e3e26c1e10a41786c3a15f5aec0ff2b8e76" + }, + { + "name": "hotdog", + "unicode": "1F32D", + "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" + }, + { + "name": "hotel", + "unicode": "1F3E8", + "digest": "428120a35b38a217901e10d704751eb8fdbc9f805e6eccd8aab070f4311b2085" + }, + { + "name": "hotsprings", + "unicode": "2668", + "digest": "df4f946218445f97a6f28c6abe4c1d1dac56ff97a8cd81df59f1b3c320e0092f" + }, + { + "name": "hourglass", + "unicode": "231B", + "digest": "07aece9413e6898717b4f0757e073d7a593f3e8044c56855127033b796207ccb" + }, + { + "name": "hourglass_flowing_sand", + "unicode": "23F3", + "digest": "92dbc68e9d16fb9f706236367e1882f0d2b6817b83ca490820a000021f2c6483" + }, + { + "name": "house", + "unicode": "1F3E0", + "digest": "a6221fc84a9b0e11ae71bfa1e0020982b55ff8c89a374a6d755dba710b4e058c" + }, + { + "name": "house_abandoned", + "unicode": "1F3DA", + "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" + }, + { + "name": "house_with_garden", + "unicode": "1F3E1", + "digest": "22d0d911da96b7ae3bf6692d3cf3590afbca959fc99c13e7a088f7194f43a35d" + }, + { + "name": "hugging", + "unicode": "1F917", + "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" + }, + { + "name": "hushed", + "unicode": "1F62F", + "digest": "69faa8e0b170ee8cf41977ca4a5154406360ed9699d5c62ecdaa01f50e8e4276" + }, + { + "name": "ice_cream", + "unicode": "1F368", + "digest": "d48ec98a8789148b96c30f19595201a0f85ed899659d97d1d3596091162909ff" + }, + { + "name": "ice_skate", + "unicode": "26F8", + "digest": "6fb044d9fbe62605f6728062c35c345ddd3ae4cc51203c925b0e69f1b3ef2dbf" + }, + { + "name": "icecream", + "unicode": "1F366", + "digest": "abd5774157575dd304dc1a393244757853972c863861a654ca29b2d528e48b28" + }, + { + "name": "id", + "unicode": "1F194", + "digest": "860ffb36d37d84e2c1cf0ab991b95c1cf73e458bef0e4d85bb0c1e26115cb2d1" + }, + { + "name": "ideograph_advantage", + "unicode": "1F250", + "digest": "37892a5642cd49ef7828646f36f48b5a83dc02437624c05da428579256118030" + }, + { + "name": "imp", + "unicode": "1F47F", + "digest": "f8c93d03bd9f1d5ef86738541e11695d6811bf6fef06759eba98321b6d038814" + }, + { + "name": "inbox_tray", + "unicode": "1F4E5", + "digest": "066a2d75633eb50329496f6866b5b0645c2e48135a03118f1bf53244f8529043" + }, + { + "name": "incoming_envelope", + "unicode": "1F4E8", + "digest": "ef6e5c5aa679d174181dae77113717f26e295778dde1e2c3bdf1d64de8a4af8c" + }, + { + "name": "info", + "unicode": "1F6C8", + "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" + }, + { + "name": "information_desk_person", + "unicode": "1F481", + "digest": "acae6d272e348aee87dd60360f16ac58cea7cb4e1ea962cc1655005c7f4aed27" + }, + { + "name": "information_desk_person_tone1", + "unicode": "1F481-1F3FB", + "digest": "709ebb0481ca981d76ece2d4fc68db693ddf18b9c1aaa0b6ac5d3c42e71bf07f" + }, + { + "name": "information_desk_person_tone2", + "unicode": "1F481-1F3FC", + "digest": "d5bc3563bc721d66b73850db93ac827be3715e7ca6420dc0051396ffe26bef47" + }, + { + "name": "information_desk_person_tone3", + "unicode": "1F481-1F3FD", + "digest": "af67fd4ef2fc402bec2d446b2e8ff5e9f636b5a9bbb6639587cdb88bd780d265" + }, + { + "name": "information_desk_person_tone4", + "unicode": "1F481-1F3FE", + "digest": "fd3174d1adfe13e8c0d6b6ae9c3a26ea35bb40f98f0728f91d1798809a74933b" + }, + { + "name": "information_desk_person_tone5", + "unicode": "1F481-1F3FF", + "digest": "4b773c443830a02de8b4d6471077b5d1387b560b537cabba7cdc667110cbde69" + }, + { + "name": "information_source", + "unicode": "2139", + "digest": "50cd8bf46d20b7c18d5f00a69fc79452aa32934245ba8d0929e51632d73876bd" + }, + { + "name": "innocent", + "unicode": "1F607", + "digest": "a3510fd51c17093ebe2371cfde7611aa44aed2d120a0e5500cfaae0f1d3486a4" + }, + { + "name": "interrobang", + "unicode": "2049", + "digest": "1f843ff672486154f9f3df549bb1b528a5eac8d15264f447649ba57f45ee4d00" + }, + { + "name": "iphone", + "unicode": "1F4F1", + "digest": "be6f96c02ddae557f700fd20fe7b3f94c9e1c928acb82b2b8b214d231273fece" + }, + { + "name": "island", + "unicode": "1F3DD", + "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" + }, + { + "name": "izakaya_lantern", + "unicode": "1F3EE", + "digest": "ddb20f475aa119c3a64a55dff40f7a9dbc3a14f7ffc6cfbac89210c652f10d02" + }, + { + "name": "jack_o_lantern", + "unicode": "1F383", + "digest": "62a701ac472619bcb3859e0d9a61b98c7f5c32150d2d04ca8c3e8fc3bec4dbd5" + }, + { + "name": "japan", + "unicode": "1F5FE", + "digest": "2535300fff2b2e4b75fc73c187be6c0ea4bc4753e443db498ea55e268e627ab7" + }, + { + "name": "japanese_castle", + "unicode": "1F3EF", + "digest": "70645aa05599e23a9ac4327e4a2e78bffe7ea06c38ec1935c15ae420619c5c1c" + }, + { + "name": "japanese_goblin", + "unicode": "1F47A", + "digest": "59b6901dc6eedc6509c25b4eef6702bf461ded06c5ff12fe2a02a5b3301577c0" + }, + { + "name": "japanese_ogre", + "unicode": "1F479", + "digest": "dab7e68cd4cbf99c13d64792c7104c4f0a846bc63aa12950fa8fab028dca301d" + }, + { + "name": "jeans", + "unicode": "1F456", + "digest": "ddd032ac77cdfe49152a0e0a0eaaaea9f183590fb1f493ec30e9e39f679e3914" + }, + { + "name": "jet_up", + "unicode": "1F6E6", + "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" + }, + { + "name": "joy", + "unicode": "1F602", + "digest": "f90cfbcb14f906f8d786b61f022c978f381fc99ca422805f605631314e101805" + }, + { + "name": "joy_cat", + "unicode": "1F639", + "digest": "6ca24a94490de66d1ca2cbc080bcd805f54ca295051d8e6588cae3fe6658c80a" + }, + { + "name": "joystick", + "unicode": "1F579", + "digest": "ec172df88ef8e8a5512d6d906c13296875b7057ed0cca79f4ac8cddd9e1de34b" + }, + { + "name": "kaaba", + "unicode": "1F54B", + "digest": "30f1a27a148399bbb811586eff795eff858701c42055c23e4d5bef7ae77f5f32" + }, + { + "name": "key", + "unicode": "1F511", + "digest": "c68ed648350d3976c8d27a709020c8873ecf553929e66453acff96231684a1a2" + }, + { + "name": "key2", + "unicode": "1F5DD", + "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" + }, + { + "name": "keyboard", + "unicode": "1F5AE", + "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" + }, + { + "name": "keyboard_mouse", + "unicode": "1F5A6", + "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" + }, + { + "name": "keyboard_with_jacks", + "unicode": "1F398", + "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" + }, + { + "name": "keycap_ten", + "unicode": "1F51F", + "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03" + }, + { + "name": "kimono", + "unicode": "1F458", + "digest": "e92bea044fe013f1993c2229d86e9cca9d43f14aab00564ce6ff559bdc5ce93a" + }, + { + "name": "kiss", + "unicode": "1F48B", + "digest": "c060eb09af2a0d0f77d307b995c15719b0e59c9162a490b8a553fac9b779c8f0" + }, + { + "name": "kiss_mm", + "unicode": "1F468-2764-1F48B-1F468", + "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" + }, + { + "name": "kiss_ww", + "unicode": "1F469-2764-1F48B-1F469", + "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" + }, + { + "name": "kissing", + "unicode": "1F617", + "digest": "3142617e8b9488689bd9efc67c0e4cc71a1870df8ffc308f949eedc5c3684051" + }, + { + "name": "kissing_cat", + "unicode": "1F63D", + "digest": "ed26cee8c438ba41365b55c48457cdad3e8d43bf90db3128ac5b277718b82ed3" + }, + { + "name": "kissing_closed_eyes", + "unicode": "1F61A", + "digest": "22d3369d21b4c2cb4c0c2cab9551cd848dd4f9adecfa64977d3f1a80fc0c8b53" + }, + { + "name": "kissing_heart", + "unicode": "1F618", + "digest": "1f089b07447bdcc1baada6a2a9607d4ef4f2de9a6093fcab47a553a64b9acb76" + }, + { + "name": "kissing_smiling_eyes", + "unicode": "1F619", + "digest": "e37d282861669adfa3953b9af833acfab7d55e787621d4318d77de7e3529d5c5" + }, + { + "name": "knife", + "unicode": "1F52A", + "digest": "3fef068a6ada61630dc868e47d25e0e0550b44bc7cf530afe88ca63dc7ab2a39" + }, + { + "name": "koala", + "unicode": "1F428", + "digest": "fe020ab9048f3c2a881474f8b1335db6bfaf37d115ff9b2d264f668d136122dd" + }, + { + "name": "koko", + "unicode": "1F201", + "digest": "734a5cb296826a598e02be3f4ec22f318633ede2ce274914586256421e2df97b" + }, + { + "name": "label", + "unicode": "1F3F7", + "digest": "9fe8195c3efab4d905b1cfcba0ae58cda12496030b0908de8076ff5e6777742e" + }, + { + "name": "large_blue_circle", + "unicode": "1F535", + "digest": "ba4d0f84a9c2be9a65b25c8cfa78f30d4856d021b1853154dd1d2fd0c5bcfb6a" + }, + { + "name": "large_blue_diamond", + "unicode": "1F537", + "digest": "d5aa5e315126859c10c83507be6b9e11cbf423f7a27145de089468cff9b94a94" + }, + { + "name": "large_orange_diamond", + "unicode": "1F536", + "digest": "108600badd0ef267842325c0fbf326cb3504306332c64f6f5694de2b54c9438a" + }, + { + "name": "last_quarter_moon", + "unicode": "1F317", + "digest": "68315b85bc1cb17bb82629bd1a6024a5124f3641b9878a732a8aad016c587546" + }, + { + "name": "last_quarter_moon_with_face", + "unicode": "1F31C", + "digest": "146a419109b7f662bf87cf9de299e47d025a8758c8970b7dabf3483e1956b559" + }, + { + "name": "laughing", + "unicode": "1F606", + "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" + }, + { + "name": "leaves", + "unicode": "1F343", + "digest": "f65e2db125564eb04fc427a49fff175d6e2dae847bd12314d5e6a131610d5ccd" + }, + { + "name": "ledger", + "unicode": "1F4D2", + "digest": "62df1772cec10c035ae0646e6cca4ba7d75b10636a520d091c5b42c2dc36b742" + }, + { + "name": "left_luggage", + "unicode": "1F6C5", + "digest": "62292758715115e55ab6239805b7f99b7b35bdfa8d40da07fe391424f1f083d8" + }, + { + "name": "left_receiver", + "unicode": "1F57B", + "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" + }, + { + "name": "left_right_arrow", + "unicode": "2194", + "digest": "28a6945972451b1f4dadec5c55310b8868ffd9f3b0a07803287bc4e07a56e7d4" + }, + { + "name": "leftwards_arrow_with_hook", + "unicode": "21A9", + "digest": "d672afc39fd50f78d7370be243173fe76ba50292f0c401305b562898939a8b7f" + }, + { + "name": "lemon", + "unicode": "1F34B", + "digest": "e0e293a8b8c1b3c87534f5e05cf006671eb3c6d52b4d17d40f2e23bce215a8be" + }, + { + "name": "leo", + "unicode": "264C", + "digest": "b0fd4e5f4637de530b62323521c6edcd80312d67ea4043eedd959acb6763474a" + }, + { + "name": "leopard", + "unicode": "1F406", + "digest": "ede891be8484a17e6277431c64ec1bfd6b742544a41947ebc85005bc2d558bb1" + }, + { + "name": "level_slider", + "unicode": "1F39A", + "digest": "49777cf160d9130d723e3bfef765c3de54033e6b059000fb0e22fb559b5ed190" + }, + { + "name": "levitate", + "unicode": "1F574", + "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" + }, + { + "name": "libra", + "unicode": "264E", + "digest": "ec8e2e7a735abc9f2bddb115fc0e09f4bdc7a164679e2b57d127f58eee1155c2" + }, + { + "name": "lifter", + "unicode": "1F3CB", + "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" + }, + { + "name": "lifter_tone1", + "unicode": "1F3CB-1F3FB", + "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" + }, + { + "name": "lifter_tone2", + "unicode": "1F3CB-1F3FC", + "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" + }, + { + "name": "lifter_tone3", + "unicode": "1F3CB-1F3FD", + "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" + }, + { + "name": "lifter_tone4", + "unicode": "1F3CB-1F3FE", + "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" + }, + { + "name": "lifter_tone5", + "unicode": "1F3CB-1F3FF", + "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" + }, + { + "name": "light_check_mark", + "unicode": "1F5F8", + "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" + }, + { + "name": "light_rail", + "unicode": "1F688", + "digest": "7c2be55456f1332e849ff6699a26dda2e1641c280f45c9ec88dedf6d9b7b7fe2" + }, + { + "name": "link", + "unicode": "1F517", + "digest": "cc4873f8a612dd721dddcd507a4430b4fb6c4abc15a8848456f0ffd97811b163" + }, + { + "name": "lion_face", + "unicode": "1F981", + "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" + }, + { + "name": "lips", + "unicode": "1F444", + "digest": "e3bc20f9e210fa1711271234fe61bf1c9ddf36dd6ffc5b832c6c3a769a1e59a8" + }, + { + "name": "lips2", + "unicode": "1F5E2", + "digest": "c6ba915982ac47d8aaf14ad3605949df95588acfb4e147bf608f8c1714cdf19b" + }, + { + "name": "lipstick", + "unicode": "1F484", + "digest": "335b912e163020df3d6d9f0a19a55d6547bd59b471c5a3e374c2968e49911ccc" + }, + { + "name": "lock", + "unicode": "1F512", + "digest": "c20eacfb8ccd9bb85919a837c0d4650ee608edb48c85bff46945f613e95d7038" + }, + { + "name": "lock_with_ink_pen", + "unicode": "1F50F", + "digest": "5cab25cea08e22d9c3f5de16de6d0ab658ca15cc93d7830f29b0f3e9348ec45f" + }, + { + "name": "lollipop", + "unicode": "1F36D", + "digest": "33d2334a00bf0e15869ccc75fadc36f27f89abf0525bb71f859aad9e1dc4ad66" + }, + { + "name": "loop", + "unicode": "27BF", + "digest": "fa1174ddc44e317d0796e07868c7ac8ac9c9274fbc8a6c3d0ec78d543c3c6bf0" + }, + { + "name": "loud_sound", + "unicode": "1F50A", + "digest": "fb70229e13b690ffc1031d2e631123f8c908035a15218c297c1c4a3ff3624aa0" + }, + { + "name": "loudspeaker", + "unicode": "1F4E2", + "digest": "e2d6cf9ec6412ee62f3128a1afd8c63ec74755c4833f01a4f99722407fe154d6" + }, + { + "name": "love_hotel", + "unicode": "1F3E9", + "digest": "184670ebc4045043a7b18d576da3255d216551da522a11cde7df34524e9c7d50" + }, + { + "name": "love_letter", + "unicode": "1F48C", + "digest": "9a4c52e2622fc7d364995ebc93ca530d972134621d117b72053a659dffc90ffc" + }, + { + "name": "low_brightness", + "unicode": "1F505", + "digest": "c177b7fa9fdbef959cc47e7d16becd71117470b767a81ed6d15f80f464776c02" + }, + { + "name": "m", + "unicode": "24C2", + "digest": "2eaf011e74d69613923dad424daaec4c13b592388dbcc5757b645bc058eedecb" + }, + { + "name": "mag", + "unicode": "1F50D", + "digest": "029427bd73d2c79fffc5194ded01f6011952ec0124b7634c6230e0afa7ad7c95" + }, + { + "name": "mag_right", + "unicode": "1F50E", + "digest": "f99de50bb59ec3bf1d4ccb8584ca09d4a7ceb5bf9f600ea8d3f84930efbf01b8" + }, + { + "name": "mahjong", + "unicode": "1F004", + "digest": "da5d1fa980c38e092d414516161ca26046aa65ace3261999ea750f72e676ac6e" + }, + { + "name": "mailbox", + "unicode": "1F4EB", + "digest": "14217df8f39a95fc0a0c527f97db1ca8564764034e921614decc5be705629352" + }, + { + "name": "mailbox_closed", + "unicode": "1F4EA", + "digest": "e0c7beb205ec548a66d8afc7f103b64c6c79c08417ab550f19c36cc6d1a62bc4" + }, + { + "name": "mailbox_with_mail", + "unicode": "1F4EC", + "digest": "6d381c0c4181be628d9409df1d85f8a9438c21ef5b92d82ef8ae1ff0079236de" + }, + { + "name": "mailbox_with_no_mail", + "unicode": "1F4ED", + "digest": "74843d5ea9e03b48323f2252bdd000585f549b7fffe1fe181a25c38b99b5e23d" + }, + { + "name": "man", + "unicode": "1F468", + "digest": "0275935258b4c832c3fcb06531d3e6972e2c3d46bab2973004750a9f00bd4cb6" + }, + { + "name": "man_tone1", + "unicode": "1F468-1F3FB", + "digest": "1f6603d040f4a025f49d384170dd16b8da169663fc3282af1dc8710d9c1a7adf" + }, + { + "name": "man_tone2", + "unicode": "1F468-1F3FC", + "digest": "d65bb03071b483946c69c61769d19b29a2af76fa7e43020e55f0bbc046492221" + }, + { + "name": "man_tone3", + "unicode": "1F468-1F3FD", + "digest": "9af8ede7211b19a7dc0c60db083dd2bdc4897dda4d71e57feadf2e39d847f060" + }, + { + "name": "man_tone4", + "unicode": "1F468-1F3FE", + "digest": "6555de60976aafeb024db78addb44eab2a412dd7277013f44d06757d03b6a252" + }, + { + "name": "man_tone5", + "unicode": "1F468-1F3FF", + "digest": "b58b97a28a6adc1777acc05194cd917c730f90e37441124c384ded12e9a7d2a4" + }, + { + "name": "man_with_gua_pi_mao", + "unicode": "1F472", + "digest": "88663173a6ccbebec5e24883c90d965447e022c6688773273110fe544d5b1607" + }, + { + "name": "man_with_gua_pi_mao_tone1", + "unicode": "1F472-1F3FB", + "digest": "3c8bad3923a619f888e14544d357499a26a517e8fbe7a51027117b960c9eb842" + }, + { + "name": "man_with_gua_pi_mao_tone2", + "unicode": "1F472-1F3FC", + "digest": "da125a3310fab19c9282497d53e2fc71ad07920ce60a0ef52dcdb31500023f09" + }, + { + "name": "man_with_gua_pi_mao_tone3", + "unicode": "1F472-1F3FD", + "digest": "1d5842558847367966bf3ea473ff80fe744359bc5d969f4cc06cf2e452ed2fb6" + }, + { + "name": "man_with_gua_pi_mao_tone4", + "unicode": "1F472-1F3FE", + "digest": "92be490f3ba602a43e2be8160d8bfd8a0691b2f81fe017b06df10f476a89ffab" + }, + { + "name": "man_with_gua_pi_mao_tone5", + "unicode": "1F472-1F3FF", + "digest": "669f6b31bc7a8bf50b169d0600f14e00addaeb24144a1bace8b94950372839b0" + }, + { + "name": "man_with_turban", + "unicode": "1F473", + "digest": "87d30d35ba40ee39c2df8ce19d975ce34a9c54688bafeac7377d7d481e55f1a4" + }, + { + "name": "man_with_turban_tone1", + "unicode": "1F473-1F3FB", + "digest": "33b8b8154e0691e2ad66177dbf1e0101411fd8b3a16bf4e54c36d4a874f2a275" + }, + { + "name": "man_with_turban_tone2", + "unicode": "1F473-1F3FC", + "digest": "1a6b83faa8d6e6a7d12a04898a6f22243287330a1faa081d2626b17dfb07174d" + }, + { + "name": "man_with_turban_tone3", + "unicode": "1F473-1F3FD", + "digest": "5d43da5109e688ff8ca0675f33ebbaf930e206f1f01e3ee773f2844663fe572b" + }, + { + "name": "man_with_turban_tone4", + "unicode": "1F473-1F3FE", + "digest": "bfaf7293c5ea75d0ecdc6fe5afe8f48e7b29b2e0df06ef974d3e1732f5db5dd4" + }, + { + "name": "man_with_turban_tone5", + "unicode": "1F473-1F3FF", + "digest": "fba2404dd3d7eab5268519894cc0b386e1b17fdf14a04760c346014aa0e25acd" + }, + { + "name": "mans_shoe", + "unicode": "1F45E", + "digest": "45dc13ac44c922b4c4b8ecb2e1a870a78e09d53da86843431ab0e9ec96ebcd97" + }, + { + "name": "map", + "unicode": "1F5FA", + "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" + }, + { + "name": "maple_leaf", + "unicode": "1F341", + "digest": "40c5ee93396301911391cf6e70454b6fa8020fe5c85d3364136bcedb5d052cdb" + }, + { + "name": "mask", + "unicode": "1F637", + "digest": "e0301cd27eb8c74c9772ff05b880215fc031ac1ae7f3177cd24ba0acb43b3834" + }, + { + "name": "massage", + "unicode": "1F486", + "digest": "856d0fb1144ee91c58dfad74f9a2cababf6bae4b3ceba2a95c03ecd44ae3aa21" + }, + { + "name": "massage_tone1", + "unicode": "1F486-1F3FB", + "digest": "fd53b06eb0967303c0914ebb79fd872900ec0f71b2852c7238517e192e5023e1" + }, + { + "name": "massage_tone2", + "unicode": "1F486-1F3FC", + "digest": "7ef57359a339ae1ca4488f9a6195a352e74daf5b67d8e1ae1e91fe866921c40c" + }, + { + "name": "massage_tone3", + "unicode": "1F486-1F3FD", + "digest": "e4fb643b6242bedb395e503ae337a88b2a255b5fda88b4aaa93396f948614a6e" + }, + { + "name": "massage_tone4", + "unicode": "1F486-1F3FE", + "digest": "94f007c2daf9455fa8d2b10cc7ccff7db9bc9daf835ef5c3699be091938db833" + }, + { + "name": "massage_tone5", + "unicode": "1F486-1F3FF", + "digest": "d18e800b728bf45b500f492062dc81312ca1ad7b1a0277a3d5bc150e4632ea1c" + }, + { + "name": "meat_on_bone", + "unicode": "1F356", + "digest": "674a2a58e174b7681eef3b6c5b39c098ed9374cc610d037166c0092ee5269a97" + }, + { + "name": "medal", + "unicode": "1F3C5", + "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" + }, + { + "name": "mega", + "unicode": "1F4E3", + "digest": "540ab4fd5bab041a681749b85e6de598ebcbfc4fbf5c3cdbd9ca1e8256191733" + }, + { + "name": "melon", + "unicode": "1F348", + "digest": "39dd0ecb23e2d3da6cbb7309333fed5d7e2cb38c0afc526ade78520eca11b5f4" + }, + { + "name": "menorah", + "unicode": "1F54E", + "digest": "5f81bc2e5a34bf76481d2958fdb0b4e4540c599aa837a6453609a39023885d8c" + }, + { + "name": "mens", + "unicode": "1F6B9", + "digest": "5ed56cff80e8ee7ed581f2a2e365915db5cb29df89e850e0add0b68db4b0c788" + }, + { + "name": "metal", + "unicode": "1F918", + "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" + }, + { + "name": "metal_tone1", + "unicode": "1F918-1F3FB", + "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" + }, + { + "name": "metal_tone2", + "unicode": "1F918-1F3FC", + "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" + }, + { + "name": "metal_tone3", + "unicode": "1F918-1F3FD", + "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" + }, + { + "name": "metal_tone4", + "unicode": "1F918-1F3FE", + "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" + }, + { + "name": "metal_tone5", + "unicode": "1F918-1F3FF", + "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" + }, + { + "name": "metro", + "unicode": "1F687", + "digest": "532378cf385f9a7fafe2f5c8203e675be6d38798871f4c8e2c50498a1529f956" + }, + { + "name": "microphone", + "unicode": "1F3A4", + "digest": "46da2b94e4dc233f640249103f09ec915aaa812cce90afe68fedb6774a27ad4b" + }, + { + "name": "microphone2", + "unicode": "1F399", + "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" + }, + { + "name": "microscope", + "unicode": "1F52C", + "digest": "79918f5fe0a39f31f270a481f4c6e00ea49fc09d64b1ae78770971293c2b1ed8" + }, + { + "name": "middle_finger", + "unicode": "1F595", + "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" + }, + { + "name": "middle_finger_tone1", + "unicode": "1F595-1F3FB", + "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" + }, + { + "name": "middle_finger_tone2", + "unicode": "1F595-1F3FC", + "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" + }, + { + "name": "middle_finger_tone3", + "unicode": "1F595-1F3FD", + "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" + }, + { + "name": "middle_finger_tone4", + "unicode": "1F595-1F3FE", + "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" + }, + { + "name": "middle_finger_tone5", + "unicode": "1F595-1F3FF", + "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" + }, + { + "name": "military_medal", + "unicode": "1F396", + "digest": "bd1da0004768f404c6bb4db85d4b748f766a77ab3edb74e709d0c0064509a043" + }, + { + "name": "milky_way", + "unicode": "1F30C", + "digest": "598b4e641c1081bb03ce38a29f9711fc8616373216a833e4daa14fbe97a358f5" + }, + { + "name": "minibus", + "unicode": "1F690", + "digest": "3d15791ca96349c3abb5bd5d1014b6b33b984db19609f56f5fd1e8d2fc551809" + }, + { + "name": "minidisc", + "unicode": "1F4BD", + "digest": "83c4bfda4e0a80785fa1c3f2bbf3c15aca2bda8ea3727ce78bc4236e1e377a36" + }, + { + "name": "mobile_phone_off", + "unicode": "1F4F4", + "digest": "cfe6dfd766b9e0b4768df25d6e943c9abc0e910ff5e5c7a8a0f425c786bbab8d" + }, + { + "name": "money_mouth", + "unicode": "1F911", + "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" + }, + { + "name": "money_with_wings", + "unicode": "1F4B8", + "digest": "f7f1fa502d2f6804169869aeb5ca7f0ea64bc2d6a0204f08875d65da4f8cb332" + }, + { + "name": "moneybag", + "unicode": "1F4B0", + "digest": "442db49cda27360d2eb781489c9879730a6094c3267bb0a0a8687d84f8fed078" + }, + { + "name": "monkey", + "unicode": "1F412", + "digest": "3141c971aacbadaba21f970a515e192740212be2a49fa1f5eb0fc4dc576e209f" + }, + { + "name": "monkey_face", + "unicode": "1F435", + "digest": "e2397431d2befe44bf5298fa81d865d80722bf954113bceacc2aa98b84d856e2" + }, + { + "name": "monorail", + "unicode": "1F69D", + "digest": "b546153200d6fbe8d65b1b34f62ff4a19b1b6a159eb1b536c5c2ecb56dab0ec9" + }, + { + "name": "mood_bubble", + "unicode": "1F5F0", + "digest": "1df7061217e478d43ab9a87d4f351c4ca56705acd6b4e0b0bedfdece77635f1b" + }, + { + "name": "mood_bubble_lightning", + "unicode": "1F5F1", + "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" + }, + { + "name": "mood_lightning", + "unicode": "1F5F2", + "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" + }, + { + "name": "mortar_board", + "unicode": "1F393", + "digest": "cb59edb08f75c374088b65284e4d0f77b9bc9573de3e6a5127f865431011e54c" + }, + { + "name": "mosque", + "unicode": "1F54C", + "digest": "a08ddb74342dea8f79063db6f98ba03eb08fe99481de8ce9123827ca7f17c7f3" + }, + { + "name": "motorboat", + "unicode": "1F6E5", + "digest": "9dbea67bbe2e95dcc68c049a58f87390a44350b32308342615d75214af3d1cef" + }, + { + "name": "motorcycle", + "unicode": "1F3CD", + "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" + }, + { + "name": "motorway", + "unicode": "1F6E3", + "digest": "fc05a36c917637c135b0a60db8afcd58cee2b335070fe3888697f8026c9d11a5" + }, + { + "name": "mount_fuji", + "unicode": "1F5FB", + "digest": "22bfffef033637b3c9b2fe7e539c74a659d2a49e594d2b33be894da00654d059" + }, + { + "name": "mountain", + "unicode": "26F0", + "digest": "486cf4e9d5f3913d138fdb7878fe869b39caa3fca53876365957a89dc8f7edb8" + }, + { + "name": "mountain_bicyclist", + "unicode": "1F6B5", + "digest": "b547b96951b6837df8ae3be1e846f15e7e2ac06d976e1fe7f1442dcc5d3a0942" + }, + { + "name": "mountain_bicyclist_tone1", + "unicode": "1F6B5-1F3FB", + "digest": "68ce0d55163c7b89ee1d87b752ece127bb25ca9deb3421b31df549a00ac5f69d" + }, + { + "name": "mountain_bicyclist_tone2", + "unicode": "1F6B5-1F3FC", + "digest": "5bfa82180bfb8bc4444cf301688aff02884895574a7ba66b398aaf20bde0f101" + }, + { + "name": "mountain_bicyclist_tone3", + "unicode": "1F6B5-1F3FD", + "digest": "33cb64a792123b81a05080465a0ea1035a2cdfdab01c71f5f725a5f92251c3e8" + }, + { + "name": "mountain_bicyclist_tone4", + "unicode": "1F6B5-1F3FE", + "digest": "9c3fa4e65dcb0ad69b963292e77c7a75853ae3c1d18a90670f81ffb65b5d020c" + }, + { + "name": "mountain_bicyclist_tone5", + "unicode": "1F6B5-1F3FF", + "digest": "871de9e3fddb49b305e5f91000143878b0288c107a125c4e60acf2b6cf8b7f3f" + }, + { + "name": "mountain_cableway", + "unicode": "1F6A0", + "digest": "f248ed5bf864f4a81e365b30d2825d2e6fc15a200c4ccf69e9f797341529f955" + }, + { + "name": "mountain_railway", + "unicode": "1F69E", + "digest": "7dd08745ab56c95c3dfcebcca517ff231cef61b670cedf9d7c53f3244c34e30b" + }, + { + "name": "mountain_snow", + "unicode": "1F3D4", + "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" + }, + { + "name": "mouse", + "unicode": "1F42D", + "digest": "fb20b3a82f407a6316bbbac68d58018c3d5b93a9a6ae968f44ace18d1c5698d9" + }, + { + "name": "mouse2", + "unicode": "1F401", + "digest": "87be4099523ec32440e6d091f1193a8ed90730b9fbecaafed4912585bfe7818c" + }, + { + "name": "mouse_one", + "unicode": "1F5AF", + "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" + }, + { + "name": "mouse_three_button", + "unicode": "1F5B1", + "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" + }, + { + "name": "movie_camera", + "unicode": "1F3A5", + "digest": "d6633b89a637b64d617c3032eed74bb82d3fa732dd9975486b2b5841b473808a" + }, + { + "name": "moyai", + "unicode": "1F5FF", + "digest": "bf948c26cd98e2f5e48da363f2924a9d7c217232115a00cec372d0d5293402a8" + }, + { + "name": "muscle", + "unicode": "1F4AA", + "digest": "c85147efb786bdea3e7d53e2edf6b827280cd9fa881661a6102a614bf5b3579f" + }, + { + "name": "muscle_tone1", + "unicode": "1F4AA-1F3FB", + "digest": "38d071df2b25031b61f3605b03c34d2e5d3e35d29f3c4aada14be37e19750eb8" + }, + { + "name": "muscle_tone2", + "unicode": "1F4AA-1F3FC", + "digest": "dcf11b76c8ffb58dc7e4f9ecd32a4c291d9772d51df2853d41081e041e7e0876" + }, + { + "name": "muscle_tone3", + "unicode": "1F4AA-1F3FD", + "digest": "a3d5f8f2dbfc28f9713ee657428ea3292c47d0b22f11a51c13594be22b0f5204" + }, + { + "name": "muscle_tone4", + "unicode": "1F4AA-1F3FE", + "digest": "eb220fc19be58d16cacc6b721e1011078b03256c0245756f251a4c2bcf50586c" + }, + { + "name": "muscle_tone5", + "unicode": "1F4AA-1F3FF", + "digest": "4e18708cbd61eaad288f913c86ad2d45108dd4484bc35879c5dcdd075eeb09fd" + }, + { + "name": "mushroom", + "unicode": "1F344", + "digest": "a2b252cd759244409d9a8066470059948e2c50b8cc86b59821c1c86b5190f640" + }, + { + "name": "musical_keyboard", + "unicode": "1F3B9", + "digest": "dcb3e84d27bfe373e5ea7ede457908de52002f0fd6105e9f3f5525c54d2a43dd" + }, + { + "name": "musical_note", + "unicode": "1F3B5", + "digest": "76a0f598f8e251a9dab44f2e14f2b7a6fb0c0c351e0f37862c8c99d380f1c261" + }, + { + "name": "musical_score", + "unicode": "1F3BC", + "digest": "a132c6b35236005b45c830a42fa97b454d3061c14991c6320f34807f10ba6a4a" + }, + { + "name": "mute", + "unicode": "1F507", + "digest": "73a99b7f9e00f92cab78cd304dee4e893a112c3a6f2285c13d44916ea547458e" + }, + { + "name": "nail_care", + "unicode": "1F485", + "digest": "62f721d3610d1647dba4b3f53cd4f2bc4180dae298314c2cca2a6a8ab1664525" + }, + { + "name": "nail_care_tone1", + "unicode": "1F485-1F3FB", + "digest": "11b82ed2e6b6619c9b74702fdacfb0ddc91310191c8b89f355c7c69a72673f8f" + }, + { + "name": "nail_care_tone2", + "unicode": "1F485-1F3FC", + "digest": "5195c76bccb9149d9080347d785dae2cce947bada5b198fae8c23e42f5553154" + }, + { + "name": "nail_care_tone3", + "unicode": "1F485-1F3FD", + "digest": "50eab0bf825c5e00db07a3f5ad26b1bb221f54efb5c55549f392b2f5aec09e5a" + }, + { + "name": "nail_care_tone4", + "unicode": "1F485-1F3FE", + "digest": "d05a9ccfad02191c89e4cbd00aa48fdaf908c0de6681f4a587d500be448e528f" + }, + { + "name": "nail_care_tone5", + "unicode": "1F485-1F3FF", + "digest": "62466354dcf6717a8b9e942ca2c5ad15a26aa815c213e3b01faba9a2e302ecdd" + }, + { + "name": "name_badge", + "unicode": "1F4DB", + "digest": "0a1cb0f7d489d3356a4d3e01f9faf78449d82d8ec4595c8639a55c3606c97c40" + }, + { + "name": "necktie", + "unicode": "1F454", + "digest": "029e1140391ef559a9316021c2db94f05653751fdf9d8f366446467a70fee6df" + }, + { + "name": "negative_squared_cross_mark", + "unicode": "274E", + "digest": "0ba0e705fdeac99edd712db31a8846320b9d2cf53c9cb4d4bcfd22ba4e1488ea" + }, + { + "name": "nerd", + "unicode": "1F913", + "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" + }, + { + "name": "network", + "unicode": "1F5A7", + "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" + }, + { + "name": "neutral_face", + "unicode": "1F610", + "digest": "df01da8501e1f588049c8ed66e504e9abcce83f74ce5790f4d3dc547408f77ee" + }, + { + "name": "new", + "unicode": "1F195", + "digest": "24e80abd29750d8b297335cdd4751b6250bb820560cf0392a6cc8783d34db63a" + }, + { + "name": "new_moon", + "unicode": "1F311", + "digest": "2d697e431eac53d6e1ea367b5da03c15fc535cd7e8c214f801fe595b768a8e11" + }, + { + "name": "new_moon_with_face", + "unicode": "1F31A", + "digest": "ea469a4668ded071f35e5898ae229fdb5d02b0730ce233169b83e22f81292baa" + }, + { + "name": "newspaper", + "unicode": "1F4F0", + "digest": "0aaf6747a43fb60cd15e6e64ca0eccaade331b376c6fe6712fd5e8294e9868cc" + }, + { + "name": "newspaper2", + "unicode": "1F5DE", + "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" + }, + { + "name": "ng", + "unicode": "1F196", + "digest": "4994c9b795033ed788e98c4af571a1dffe28c0a1479e3b42dcae21bb08381b5f" + }, + { + "name": "night_with_stars", + "unicode": "1F303", + "digest": "56bb4a59a897c1836ee1a49cc99f468891b790b0f8bce203c201c13bb7b8ae9a" + }, + { + "name": "nine", + "unicode": "0039-20E3", + "digest": "7e3644a98cb6417a351530c9ce6b368e637a22c847a8c04133897dc1c5d7419f" + }, + { + "name": "no_bell", + "unicode": "1F515", + "digest": "f4fb42836132000101624fecef8b9358736a0fc76beae460e6986aaa479204fd" + }, + { + "name": "no_bicycles", + "unicode": "1F6B3", + "digest": "b3c258bea7d6988640e3348598c03c97632ca00a11cbf0352995b801ff4a296b" + }, + { + "name": "no_entry", + "unicode": "26D4", + "digest": "ac807d54092efdc3aea417790a7d0c50b59800c9ea49b37f1aec6d2e453c5f6d" + }, + { + "name": "no_entry_sign", + "unicode": "1F6AB", + "digest": "5a17d677ec1c7595a7970a1cbe0d20909341b30d3ab31471ced590f51fff1ff7" + }, + { + "name": "no_good", + "unicode": "1F645", + "digest": "8ce921e5e13e1203cf43fdc3e7c5ec1fb2a1f9ff79f21539cff542c80af2e5fe" + }, + { + "name": "no_good_tone1", + "unicode": "1F645-1F3FB", + "digest": "aab4d354aaac06e8348eb354487c6381e475b44651cb2716660904a36c47a1b6" + }, + { + "name": "no_good_tone2", + "unicode": "1F645-1F3FC", + "digest": "8fb66b1a7b8f72062794281294515d47471a8c59de300b99d656c3412ca19d64" + }, + { + "name": "no_good_tone3", + "unicode": "1F645-1F3FD", + "digest": "aeecf73fb9dca24b4002db2802fc9b5a483644c49f834c19f143d4e56ec46c1a" + }, + { + "name": "no_good_tone4", + "unicode": "1F645-1F3FE", + "digest": "fadeb23307d5ccabbf08c848cf81c66c05b152aa32b85f86061caf14760f8eb9" + }, + { + "name": "no_good_tone5", + "unicode": "1F645-1F3FF", + "digest": "cf26d5d6463d0febf4e1f08e343308742ffe0811cfc30c459b87d4cc812f5d04" + }, + { + "name": "no_mobile_phones", + "unicode": "1F4F5", + "digest": "3b4ead88beca33f1e303d0a45268849be7aaaff7830b761732c7a5afc5a2de3a" + }, + { + "name": "no_mouth", + "unicode": "1F636", + "digest": "2af81a3e07a8b7827a1e58f6f5036ccff2f6e7b0027a4f934c9fa34c6a780963" + }, + { + "name": "no_pedestrians", + "unicode": "1F6B7", + "digest": "9f9ed90bb8f9964fa8cb0048fc092ecc0612a1994c98d19ef0b5a58607888476" + }, + { + "name": "no_smoking", + "unicode": "1F6AD", + "digest": "fb90290ff5c917b7307a97c8ba793d20c61042525cf2f7bfd4cd2a7878aeefa5" + }, + { + "name": "non-potable_water", + "unicode": "1F6B1", + "digest": "c4ddca2ab1a97260e9b2c2aa33fb03455c0e8174541c3a9416fc44143a3ee567" + }, + { + "name": "nose", + "unicode": "1F443", + "digest": "308e28b15b7f734f6f184ae367789d7cf258656b24861cf8d5935127d810aa3f" + }, + { + "name": "nose_tone1", + "unicode": "1F443-1F3FB", + "digest": "392d24b38ac3edc2d7b83945a60bbe9115a6a97658d8af35281a7cbef79449e8" + }, + { + "name": "nose_tone2", + "unicode": "1F443-1F3FC", + "digest": "409a790339c405770492e49fdb0b5ba34087c27e2f9018ecd845ab078e61476a" + }, + { + "name": "nose_tone3", + "unicode": "1F443-1F3FD", + "digest": "92b52b479a935f31e460257d809c531edad1a6bb4583ad18233b12c4e45202fe" + }, + { + "name": "nose_tone4", + "unicode": "1F443-1F3FE", + "digest": "78ad2e857792e86cded6ba5620f634f7d1f79a92c82c266e48fab9bd73df3688" + }, + { + "name": "nose_tone5", + "unicode": "1F443-1F3FF", + "digest": "dbef6813c1965d3e93f70f33f118f9950130af21c622cea97ea215a36b4fa73f" + }, + { + "name": "note", + "unicode": "1F5C9", + "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" + }, + { + "name": "note_empty", + "unicode": "1F5C6", + "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" + }, + { + "name": "notebook", + "unicode": "1F4D3", + "digest": "64bd4a3e7ca7b22fc704c7b7bd4d13540c16bc69b9d8dd76e69e6ad573ab3823" + }, + { + "name": "notebook_with_decorative_cover", + "unicode": "1F4D4", + "digest": "4b45f28fbde1be5c214a6bc2413abc91db02bccd86f74c21b7f4a4da8b75a46f" + }, + { + "name": "notepad", + "unicode": "1F5CA", + "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" + }, + { + "name": "notepad_empty", + "unicode": "1F5C7", + "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" + }, + { + "name": "notepad_spiral", + "unicode": "1F5D2", + "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" + }, + { + "name": "notes", + "unicode": "1F3B6", + "digest": "bf3868386e17eac40ac7fbabea027042027ff061daafe406c869cdd8ce94641d" + }, + { + "name": "nut_and_bolt", + "unicode": "1F529", + "digest": "fdb9d7408202fad7a52ff21608042c08c3b0beb195999fff233df36a29dc9e96" + }, + { + "name": "o", + "unicode": "2B55", + "digest": "8e119dba4130bd33b3ee5c862fb4fa5a691173911ffee51cb9359fee3398e330" + }, + { + "name": "o2", + "unicode": "1F17E", + "digest": "00d751124c25633611055bd61e74fc3f3d1779f0d09e1e707837686f613367b4" + }, + { + "name": "ocean", + "unicode": "1F30A", + "digest": "9b1fbfd2a64f417d0c2cb91085b29a12d14e15844bc21798bdee938bb7bf6222" + }, + { + "name": "octopus", + "unicode": "1F419", + "digest": "3fdfbc02f47ad434bdeb7f3a15cd4e8f8118ee1cd754627e358f1c2f4616f5e3" + }, + { + "name": "oden", + "unicode": "1F362", + "digest": "afed1c5166943e5803602ffacc67652e3b29ee4222a6c36aba2daf88bd21ad3c" + }, + { + "name": "office", + "unicode": "1F3E2", + "digest": "dc1836ef152d88fd628df18db770594f5dbc8d7f20d6ce982588b25b78b19c92" + }, + { + "name": "oil", + "unicode": "1F6E2", + "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" + }, + { + "name": "ok", + "unicode": "1F197", + "digest": "6b05bbab4a7104541c2f4bce553884d17ae0ad07589b19d6b53b6949c14f2269" + }, + { + "name": "ok_hand", + "unicode": "1F44C", + "digest": "9981f32ef200b011a10f6bfa2066c41b6b5e7bcd6c3c21647980b640bc1fa93b" + }, + { + "name": "ok_hand_tone1", + "unicode": "1F44C-1F3FB", + "digest": "e5933a9b64b03ce0634f15f02ff7b6424530dbdc0e283461e0c9992d0c2ca2ad" + }, + { + "name": "ok_hand_tone2", + "unicode": "1F44C-1F3FC", + "digest": "4c04741c9f2c8731da8df3015e9aae00061a01848c2d22aab1e9853c271deed3" + }, + { + "name": "ok_hand_tone3", + "unicode": "1F44C-1F3FD", + "digest": "216dc5a72f9e34bbb7b39f680c388bd5b52abf9b41b843342e53e285b7933076" + }, + { + "name": "ok_hand_tone4", + "unicode": "1F44C-1F3FE", + "digest": "7139de7ec9d5a962cf87b9fbbeef3a53aa482bb840ab3b64d8d0da81bdc19886" + }, + { + "name": "ok_hand_tone5", + "unicode": "1F44C-1F3FF", + "digest": "e18b0a1bc5d970cc63466bd6da6e9f855db37d1eada3230d19f600c1f5a402a3" + }, + { + "name": "ok_woman", + "unicode": "1F646", + "digest": "3b2fa732d9c9addb056f136192428e99d805d4cb1c7dab724fd552c7e93197e4" + }, + { + "name": "ok_woman_tone1", + "unicode": "1F646-1F3FB", + "digest": "017aca3797701b043a44f22e67dcad8b531a3ca14e629ae0d2fbc601ed3e49cb" + }, + { + "name": "ok_woman_tone2", + "unicode": "1F646-1F3FC", + "digest": "036bed032bc5a616668775cda0d5640c810e2836aa28009c8e8bf2b487259c59" + }, + { + "name": "ok_woman_tone3", + "unicode": "1F646-1F3FD", + "digest": "d9a4414caddda43d1a36828cfbecce5f2b7e5c1b67b4a47991b2ae0a34cf7ab7" + }, + { + "name": "ok_woman_tone4", + "unicode": "1F646-1F3FE", + "digest": "942e1b9aa495c4c4de0804e4d4348422201299d649e5d65829ba4a308880df1c" + }, + { + "name": "ok_woman_tone5", + "unicode": "1F646-1F3FF", + "digest": "e8d0fb5b999d5d63404493aa505b5af2260c76001023431d5e788773d0a9e2de" + }, + { + "name": "older_man", + "unicode": "1F474", + "digest": "620f763325827acbeb9d57798ef55d87827d0dfc77b84d942e25bc5057f2cbfe" + }, + { + "name": "older_man_tone1", + "unicode": "1F474-1F3FB", + "digest": "e0f35c12362eae503d1c30a345c3a4978196d351d8a1eb9d5f107c60ea4bbf52" + }, + { + "name": "older_man_tone2", + "unicode": "1F474-1F3FC", + "digest": "671766ce9fa47c3fa009d4f138344c87d73032a1c38e48614c663f8ea5d0f673" + }, + { + "name": "older_man_tone3", + "unicode": "1F474-1F3FD", + "digest": "6ff4885ef8c416b8970780a691fef74c8d89421ab11e0aa8c522c33e1c67fbe8" + }, + { + "name": "older_man_tone4", + "unicode": "1F474-1F3FE", + "digest": "0ae7d4e316dcd4d27a5a6cdaabab88a4f992bd1b75f6ceaeb5b906ed1eb5269c" + }, + { + "name": "older_man_tone5", + "unicode": "1F474-1F3FF", + "digest": "abe2757bd5e35f30d2a6daec09637ea5382a46d14d239b77282e9bf874229b57" + }, + { + "name": "older_woman", + "unicode": "1F475", + "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" + }, + { + "name": "older_woman_tone1", + "unicode": "1F475-1F3FB", + "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" + }, + { + "name": "older_woman_tone2", + "unicode": "1F475-1F3FC", + "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" + }, + { + "name": "older_woman_tone3", + "unicode": "1F475-1F3FD", + "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" + }, + { + "name": "older_woman_tone4", + "unicode": "1F475-1F3FE", + "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" + }, + { + "name": "older_woman_tone5", + "unicode": "1F475-1F3FF", + "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" + }, + { + "name": "om_symbol", + "unicode": "1F549", + "digest": "c8c1c9d445b1fc50a627b71bee21fba978e04532e4685ec032a0174f51fc12bb" + }, + { + "name": "on", + "unicode": "1F51B", + "digest": "08e1159a68d3334a87ffa75b9e70826cb557d0f73a2c1d08f4c3d60476ecacc8" + }, + { + "name": "oncoming_automobile", + "unicode": "1F698", + "digest": "6bff7f40fe223df6d16c7512532b8aa6f83e8c13e1007b63eb9aabf774c1a322" + }, + { + "name": "oncoming_bus", + "unicode": "1F68D", + "digest": "127a357fcd96ce4b9ab11c3dba95d8ff811bab193dd8ba38efb7067a44752ce8" + }, + { + "name": "oncoming_police_car", + "unicode": "1F694", + "digest": "57cb70e05e70c1f68ab42259f307ed9782c2b9d6e35d2dff2895aa23d7eb6b04" + }, + { + "name": "oncoming_taxi", + "unicode": "1F696", + "digest": "174967ae4c3d5881d2408c71c020f704e933190af4caef5d2908e9ac382f35ea" + }, + { + "name": "one", + "unicode": "0031-20E3", + "digest": "113b9d87c3e37c9c54e49cecccbfc40c15fb97fd03a51505df85e48b78702b2b" + }, + { + "name": "open_file_folder", + "unicode": "1F4C2", + "digest": "def93715203aed464211798d773732895a19389a94a2e7ed43e7f229b2aab7da" + }, + { + "name": "open_hands", + "unicode": "1F450", + "digest": "7c60a37ae11727c998908199b8709e52593b931843aef942f37b306b1edca12a" + }, + { + "name": "open_hands_tone1", + "unicode": "1F450-1F3FB", + "digest": "09ffa9b3f28fc56a71e4e711bdfc87ce1a56721229377e71f1c00224523f8b9b" + }, + { + "name": "open_hands_tone2", + "unicode": "1F450-1F3FC", + "digest": "21ecaba9f086bcb7eb07c17c2b2621bcd1ca28c57f79032d5e0eba356494cc85" + }, + { + "name": "open_hands_tone3", + "unicode": "1F450-1F3FD", + "digest": "c7dbb8c44f78f7793b202ec215fee42b7e1e555d659fbf402383500217b89656" + }, + { + "name": "open_hands_tone4", + "unicode": "1F450-1F3FE", + "digest": "867451d42492ab2277687447f421f744530b9ea057312326353fec39c94b18fd" + }, + { + "name": "open_hands_tone5", + "unicode": "1F450-1F3FF", + "digest": "56335506cf68e29150cb68d7ebbb4a92aed390018966669a8144d20ae0d6cfe3" + }, + { + "name": "open_mouth", + "unicode": "1F62E", + "digest": "f05fdf998e8b5c0b00ebd8b5ab17a67f5c0a45275f31a201af74e8ab0c2f7ba9" + }, + { + "name": "ophiuchus", + "unicode": "26CE", + "digest": "98c61bb0c36d60c476d42d5e074297662e8d141dcab7004a5bd63c359eed3b84" + }, + { + "name": "optical_disk", + "unicode": "1F5B8", + "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" + }, + { + "name": "orange_book", + "unicode": "1F4D9", + "digest": "86d150ea3d62183ab7dfe2851cf7f4d1ae769b7ecbb1987b0f463e639e429598" + }, + { + "name": "orthodox_cross", + "unicode": "2626", + "digest": "9c861285ca6d699cd2c72b6df44ec2b1e64138152f19c66e32df1ce770ff2e83" + }, + { + "name": "outbox_tray", + "unicode": "1F4E4", + "digest": "b6a6015d5d7d528af485de23ff4518dc35408def1cc49bc6c9b01d880d613985" + }, + { + "name": "ox", + "unicode": "1F402", + "digest": "cbcfe5c8c4d6b939e24e18e610785f171bb9410441e02c2eeb1bceb0a6246daf" + }, + { + "name": "package", + "unicode": "1F4E6", + "digest": "4023cffce85384217a73609f457aec013876e689c44bcfff0bcc35f3e4e1ab00" + }, + { + "name": "page", + "unicode": "1F5CF", + "digest": "cc745056525f59d9128d1d03b14770376bb09ab64b8ef4ac994ab7f38efd4783" + }, + { + "name": "page_facing_up", + "unicode": "1F4C4", + "digest": "71a0872bf1b13c58746f9b41655227c75be107ab6083c0dce13cb16444af22e7" + }, + { + "name": "page_with_curl", + "unicode": "1F4C3", + "digest": "cb4210464faea946c7b07db7067c7fc98920f778cf57721388f5362942ba3029" + }, + { + "name": "pager", + "unicode": "1F4DF", + "digest": "209dbdc19aa650ecacc0569e17a9123c9a1e39df59c9b4120f3b0888b63cd6f1" + }, + { + "name": "pages", + "unicode": "1F5D0", + "digest": "05bd47b78f089389356d9d839c736843f56b959ab4277056606ffcbb013390bc" + }, + { + "name": "paintbrush", + "unicode": "1F58C", + "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" + }, + { + "name": "palm_tree", + "unicode": "1F334", + "digest": "1589ff4b1b87296edc0118e4aa67b3b504ed85a5b8d47e7d0c3e309d0bbf8cd6" + }, + { + "name": "panda_face", + "unicode": "1F43C", + "digest": "050ee87892f56ff485f460bc6c3846d98a0ca7083d2cf0b8ab24772b672273f2" + }, + { + "name": "paperclip", + "unicode": "1F4CE", + "digest": "1463607a59345973f009fa53a719e2264b95743560adb99737bef29b1d133a95" + }, + { + "name": "paperclips", + "unicode": "1F587", + "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" + }, + { + "name": "park", + "unicode": "1F3DE", + "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" + }, + { + "name": "parking", + "unicode": "1F17F", + "digest": "e1d2cfd1c57ea85003ca4df066cbba4e506bf6c4d6c790e27b2f78ad8443fabf" + }, + { + "name": "part_alternation_mark", + "unicode": "303D", + "digest": "b3cc2e803b255e858417345ba6ba52a1c22f511b483fec11b5d68c4432f759b6" + }, + { + "name": "partly_sunny", + "unicode": "26C5", + "digest": "484990f5e1a3b14c731e7bd4b0b4a1c10cd5fb54ac7cf2751f40c8bf59d7e2b4" + }, + { + "name": "passport_control", + "unicode": "1F6C2", + "digest": "224e8ef60d4d6587721727555de324948fb5b6c1cb5cc4b546960983d1ec85c4" + }, + { + "name": "pause_button", + "unicode": "23F8", + "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" + }, + { + "name": "peace", + "unicode": "262E", + "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" + }, + { + "name": "peach", + "unicode": "1F351", + "digest": "a3f4fd5ff02e0a03104ab54456ee1a7521858ee68443856ee10e0972e5b6aaa5" + }, + { + "name": "pear", + "unicode": "1F350", + "digest": "7a7a72568d53677cd1fff4d9e58e63327a742fa16d22a2bef03b4a6fa378d3b3" + }, + { + "name": "pen_ballpoint", + "unicode": "1F58A", + "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" + }, + { + "name": "pen_fountain", + "unicode": "1F58B", + "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" + }, + { + "name": "pencil", + "unicode": "1F4DD", + "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" + }, + { + "name": "pencil2", + "unicode": "270F", + "digest": "aa2c572772187fee1f9125bb0950f5ce8a61f7dd2647258c40b4077ee5feb498" + }, + { + "name": "pencil3", + "unicode": "1F589", + "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" + }, + { + "name": "penguin", + "unicode": "1F427", + "digest": "095de34b3f6a2521a342c21f5f2551a0092bf47429801c15b7bbf0913924f412" + }, + { + "name": "pennant_black", + "unicode": "1F3F2", + "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" + }, + { + "name": "pennant_white", + "unicode": "1F3F1", + "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" + }, + { + "name": "pensive", + "unicode": "1F614", + "digest": "2d9e7f1eed14dcc86674cec78e992567a40d0f223fc67d722b91eebcd1251269" + }, + { + "name": "performing_arts", + "unicode": "1F3AD", + "digest": "a202755bab6427433975589bb8b63e61e5d7f55c6242676d8000e91eedabc55e" + }, + { + "name": "persevere", + "unicode": "1F623", + "digest": "686ef3fc70ce8294d02a764ebd75b69f25cca6bff6b92e7905130366d22f6d8a" + }, + { + "name": "person_frowning", + "unicode": "1F64D", + "digest": "16e8fbf22c0b4c237d0d45202fa32d1ebd04760a5b6975c9c9b477321ccb0e12" + }, + { + "name": "person_frowning_tone1", + "unicode": "1F64D-1F3FB", + "digest": "a143b865976ce3cf307db854cfd1ca58c3832df0eee5e9b0ab307cf4f24ba3db" + }, + { + "name": "person_frowning_tone2", + "unicode": "1F64D-1F3FC", + "digest": "4e7050d8a38019ba2293f66b9930e6a7e35dacf3b3bc9431edb586a0d9ea8054" + }, + { + "name": "person_frowning_tone3", + "unicode": "1F64D-1F3FD", + "digest": "0750015d3ac1b5954d31e36cd59c70b6ed9f4df698082484b7ac59eb0b9964b0" + }, + { + "name": "person_frowning_tone4", + "unicode": "1F64D-1F3FE", + "digest": "18d6cc92d0990624218d38d6eeed60bccb371d0fc9f1c889e9476b3b0c44b5e8" + }, + { + "name": "person_frowning_tone5", + "unicode": "1F64D-1F3FF", + "digest": "4a898199cbaf083d37511f51d8a1d2560b7a20c62a1b09087831da7010fbd093" + }, + { + "name": "person_with_blond_hair", + "unicode": "1F471", + "digest": "67d95a0801c65f62db55fa80ab35dec65c239601a44bf5f5902e4645f126770e" + }, + { + "name": "person_with_blond_hair_tone1", + "unicode": "1F471-1F3FB", + "digest": "e79717bfe30a26eafc082a75fa7547d8f2ad3c123fb2d75a95e75f0ce7ecbd0c" + }, + { + "name": "person_with_blond_hair_tone2", + "unicode": "1F471-1F3FC", + "digest": "c4a1961c292149ab6e1fd54a7894398599bf855de97a05ee4e836a86a400deb3" + }, + { + "name": "person_with_blond_hair_tone3", + "unicode": "1F471-1F3FD", + "digest": "e2707d0cf778bee5b72d861ec76430eb1cf9f9820f066ee6327574d5697f445e" + }, + { + "name": "person_with_blond_hair_tone4", + "unicode": "1F471-1F3FE", + "digest": "94da43f0b12ef4a98dabec096ff1184b0a9b5b6ee55824d257e5112cc7e88730" + }, + { + "name": "person_with_blond_hair_tone5", + "unicode": "1F471-1F3FF", + "digest": "9e096a210ea720d32bc6a7005cd77f8b314ccf817fc3060da2e1796de39e9d60" + }, + { + "name": "person_with_pouting_face", + "unicode": "1F64E", + "digest": "8c3199a422250d2db9a163156191ed2c6697d7f31699e2efe19e05ca26e5d225" + }, + { + "name": "person_with_pouting_face_tone1", + "unicode": "1F64E-1F3FB", + "digest": "3e1f09bbf607381c992739ea92dd35cbd26b1bbc705a7d21b7c3156f50e9d8b3" + }, + { + "name": "person_with_pouting_face_tone2", + "unicode": "1F64E-1F3FC", + "digest": "b5fc1cf3fdc5ff01105ee2452db90baa6a52c1e42f3795b2836c3e35197ece1f" + }, + { + "name": "person_with_pouting_face_tone3", + "unicode": "1F64E-1F3FD", + "digest": "e8ec2539c458a8283c8c1050634c432b6363f3e64b68ba4c977994782f09b564" + }, + { + "name": "person_with_pouting_face_tone4", + "unicode": "1F64E-1F3FE", + "digest": "5cab7a29699decd45682583446c2bf56ddcd69cd16e14db661b526a4076dfa17" + }, + { + "name": "person_with_pouting_face_tone5", + "unicode": "1F64E-1F3FF", + "digest": "3caebd3626fd77d849859d1c99a747f80a2b59bfa5c1854494f1ce0485539a94" + }, + { + "name": "pick", + "unicode": "26CF", + "digest": "24a3e8f592435b97272e6d134ea5503dce3012811659c4aadbad4e45d9fba679" + }, + { + "name": "pig", + "unicode": "1F437", + "digest": "50b55fc74e8f6c89c6e04609381c99a660748908f0ef015f5da37089678ad0c3" + }, + { + "name": "pig2", + "unicode": "1F416", + "digest": "e8189fb678608e8b9d69e11d2566f9a4765cbdff99ec8e66df30c7a2dabf742f" + }, + { + "name": "pig_nose", + "unicode": "1F43D", + "digest": "7e299cb49a771884f5065c68733a5a1fe354a54cff009127230177f1717af4a5" + }, + { + "name": "pill", + "unicode": "1F48A", + "digest": "53ae3379cc6721744979122569f157a5a13aa6b48e081a89f17b2d90134efe9e" + }, + { + "name": "pineapple", + "unicode": "1F34D", + "digest": "ceda8ffa4a41594f28a4e69d03f8a1daeb2ba20740f0b8c56447cae833eea035" + }, + { + "name": "ping_pong", + "unicode": "1F3D3", + "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" + }, + { + "name": "piracy", + "unicode": "1F572", + "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" + }, + { + "name": "pisces", + "unicode": "2653", + "digest": "75f11b9a094196b54a242420362fa7c0aeba7cfc497b187e1aaaba96d93684a7" + }, + { + "name": "pizza", + "unicode": "1F355", + "digest": "ac94ae1c034f7b854ce2a483e1c219d101a84336f5065342f4824ff32ba705c4" + }, + { + "name": "place_of_worship", + "unicode": "1F6D0", + "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" + }, + { + "name": "play_pause", + "unicode": "23EF", + "digest": "d69e8cdec33447283cf65d343b986115e27681d781b721db7894e5c587ca18ad" + }, + { + "name": "point_down", + "unicode": "1F447", + "digest": "685f46a643be7f3033896e59a822f87d61ce50db6969bcdbacc743215a96bb7a" + }, + { + "name": "point_down_tone1", + "unicode": "1F447-1F3FB", + "digest": "d3dd2608fe17d5649c960fcf8dbdb68466908d80fa349b7947b457da2a27ebb1" + }, + { + "name": "point_down_tone2", + "unicode": "1F447-1F3FC", + "digest": "67ab236a14f6d63abcdb26433a66a183d223186c21ebc9f978fab50165ebe271" + }, + { + "name": "point_down_tone3", + "unicode": "1F447-1F3FD", + "digest": "c8a2368f2cedb5bbb5cc0195b97fbf3787747637bf6e77bdc9a4edf4a3f22a04" + }, + { + "name": "point_down_tone4", + "unicode": "1F447-1F3FE", + "digest": "6a92eab3bc8f950fa423e690f54a352887bda92f01e91c62eb3f3a9544c10cd8" + }, + { + "name": "point_down_tone5", + "unicode": "1F447-1F3FF", + "digest": "6ad329f156414f421d6f8cf5e2a68d34b7a41f90d80e8e66b15bcbd3788126c7" + }, + { + "name": "point_left", + "unicode": "1F448", + "digest": "cb520d6bba4c2b3bd7911315c9efce3261d048ff090437d7e24c9c5a7255043e" + }, + { + "name": "point_left_tone1", + "unicode": "1F448-1F3FB", + "digest": "81813901bdaa8d261277f79aff9e9a21beb80a5855899941820b25f70786ec21" + }, + { + "name": "point_left_tone2", + "unicode": "1F448-1F3FC", + "digest": "ecdc3dea0d644290aa7e0dab758c215822482a482ba35d825a33152453593c1e" + }, + { + "name": "point_left_tone3", + "unicode": "1F448-1F3FD", + "digest": "84e73b6a37755016271c255eba164f349dbd2a2badf5d9ac1c6f4cbfcae589f0" + }, + { + "name": "point_left_tone4", + "unicode": "1F448-1F3FE", + "digest": "d16800499b6c6ede94256796b1de8a8f723879f636849856b3bd8b7a092b5576" + }, + { + "name": "point_left_tone5", + "unicode": "1F448-1F3FF", + "digest": "18b7108066cebf2d4090f29e595a2f01db94bd210f3b1d61dc269ec249a749b9" + }, + { + "name": "point_right", + "unicode": "1F449", + "digest": "866180bf31e92de32aba336d5b5ce81773a29cdaadada1d93c944cf9ad9783bc" + }, + { + "name": "point_right_tone1", + "unicode": "1F449-1F3FB", + "digest": "ebe2e4bf6bd46a5798b9a845a4ed055911c4fe58dbeacc4d39d6ea63e28e7cc9" + }, + { + "name": "point_right_tone2", + "unicode": "1F449-1F3FC", + "digest": "b638662a67b1c6adde4f5abc789aae010b178404cdd1b71fcc982cdf8307c655" + }, + { + "name": "point_right_tone3", + "unicode": "1F449-1F3FD", + "digest": "32c6ca2f992416ab2c36672dfbc1c0de8f102c77a13496dd8d63736a7b0261d2" + }, + { + "name": "point_right_tone4", + "unicode": "1F449-1F3FE", + "digest": "89bd6828e9b82408a3829d49fa43332e2599f7d10bc6e5b14b750ef03267b173" + }, + { + "name": "point_right_tone5", + "unicode": "1F449-1F3FF", + "digest": "390525048a12b0efa22de550c800e439b0deaad03f1f31155d179aef093354af" + }, + { + "name": "point_up", + "unicode": "261D", + "digest": "31b5ca1303c1afabe1db322b24f73b23f3568c87a364f61c82f6e0c858c090e9" + }, + { + "name": "point_up_2", + "unicode": "1F446", + "digest": "55c237054aa347c9847f3f3f577eb755db55dfcf793aa7de0f8f868574d70e8f" + }, + { + "name": "point_up_2_tone1", + "unicode": "1F446-1F3FB", + "digest": "dc07e7732d973de96ae3b08b14c19e20b6c1aea7f5a30e7198679b750422e914" + }, + { + "name": "point_up_2_tone2", + "unicode": "1F446-1F3FC", + "digest": "af2211fc4a1bd51d1e76f7bc43a6fa87bdd24e4295c52fdbdb01c1ca670a6cd7" + }, + { + "name": "point_up_2_tone3", + "unicode": "1F446-1F3FD", + "digest": "917701169b3fb3e1b6e14a68e9572b25998ef2e38abac9ad8cf30100f8ea0dac" + }, + { + "name": "point_up_2_tone4", + "unicode": "1F446-1F3FE", + "digest": "20843904764c6c3e55792cce0c55c76f72b97788c5229cad655ebf1f2873b439" + }, + { + "name": "point_up_2_tone5", + "unicode": "1F446-1F3FF", + "digest": "1d0cca546027c717da50f90da65757af46fe7cd4e397da9b8e203446f707208d" + }, + { + "name": "point_up_tone1", + "unicode": "261D-1F3FB", + "digest": "5ede60379dee23166c6b834d73da8b55268e330f67058843b8a3705dca6ed71a" + }, + { + "name": "point_up_tone2", + "unicode": "261D-1F3FC", + "digest": "c94a15ef848d410aa5d32b8d0e453b59682fde6f39e6705cbb81cf0829833a81" + }, + { + "name": "point_up_tone3", + "unicode": "261D-1F3FD", + "digest": "d319ce72876d97a3b1d4bc7c0679e546a983f02145d723a0da5ed0b73a51cfe7" + }, + { + "name": "point_up_tone4", + "unicode": "261D-1F3FE", + "digest": "9171a27f86f27fd144347a17153fb56e30bd32e67a8f10f8c1f32a40cad4e009" + }, + { + "name": "point_up_tone5", + "unicode": "261D-1F3FF", + "digest": "a894f87da4c3d33d5e6e74d003a33ec60c453db6507fe05d22235f807ead27d6" + }, + { + "name": "police_car", + "unicode": "1F693", + "digest": "7999869cb75be404fc34942b6f9d8e84fa7e259aa892a1e8e1652a5f02cceea6" + }, + { + "name": "poodle", + "unicode": "1F429", + "digest": "8a568d8688bf19b440b7c1b49fcfe6672b8f75af0031d89ab6212623430acadb" + }, + { + "name": "poop", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "popcorn", + "unicode": "1F37F", + "digest": "12264cb16fca9317e3ba8d5924a2c8f15f790e36d2f29e7b12aaaf77e1beb73d" + }, + { + "name": "post_office", + "unicode": "1F3E3", + "digest": "5e2d896cd646a2eecd5596af9e44ca1fa2745de5cedaf0f6d193b8243201c6cc" + }, + { + "name": "postal_horn", + "unicode": "1F4EF", + "digest": "339aa61fa1567a1d159bb8204d15db889fbb6cc1106f6e1991b4a184d1bc1fc7" + }, + { + "name": "postbox", + "unicode": "1F4EE", + "digest": "ef1a6543fccb9f1009cc3782c51883e51167721a0b49e8ba21e8e6049b216906" + }, + { + "name": "potable_water", + "unicode": "1F6B0", + "digest": "4a2379835660dfa8b6780d662a10d1effab710f471eb9b5e6ade4772ba7e5aeb" + }, + { + "name": "pouch", + "unicode": "1F45D", + "digest": "cbd47ec1a65f5c642773d8ea2e7e57f7041a2d7ed9df05fbdd7bc8743c6dece6" + }, + { + "name": "poultry_leg", + "unicode": "1F357", + "digest": "d416e9464bd58073bd3e32eb06c0da96905609f47b9d667acdc0810e94237584" + }, + { + "name": "pound", + "unicode": "1F4B7", + "digest": "1ac491bb8a91613b2b1faaac4e7b4bc794d2abef69ac79de17d54c824c3ef826" + }, + { + "name": "pouting_cat", + "unicode": "1F63E", + "digest": "ba28d75401d5bb98773acd35aaf173356bae4d5a5520a226559478138364ebdf" + }, + { + "name": "pray", + "unicode": "1F64F", + "digest": "fb0df9c1566014bd2df2a1afd59366b896f20c03ca3516e02e4be44ea556c8ea" + }, + { + "name": "pray_tone1", + "unicode": "1F64F-1F3FB", + "digest": "c6d8cb46e65ad13a92e85f97e018176fd89513f23e899e15d1ad09e3b4009f4b" + }, + { + "name": "pray_tone2", + "unicode": "1F64F-1F3FC", + "digest": "2cd68cbe1ba3254f173ec8136af79cae64873bd0f20480158c3e6babd5a1a442" + }, + { + "name": "pray_tone3", + "unicode": "1F64F-1F3FD", + "digest": "d2e81863f74a87b96335fb108e7b206f28ed18185362ab4d42a3b0523801398b" + }, + { + "name": "pray_tone4", + "unicode": "1F64F-1F3FE", + "digest": "ad1b91254b101d872325c325ebd1f2a6257cfe22e83de88e29dd16ffac191979" + }, + { + "name": "pray_tone5", + "unicode": "1F64F-1F3FF", + "digest": "23f40a11321decbdc6a1d274b9ad571041d261d364d13d1063c306e73ad52254" + }, + { + "name": "prayer_beads", + "unicode": "1F4FF", + "digest": "cb6f8700154f75749cf2642a25c03e255dc18428baf8b57f6bd807c92b83e28d" + }, + { + "name": "princess", + "unicode": "1F478", + "digest": "47b93eb52d757c3c000d9760391ecb942776d883b28050d833fa11612483d8ee" + }, + { + "name": "princess_tone1", + "unicode": "1F478-1F3FB", + "digest": "1e4073c2abdf51a61a1a85a3e063541fe96e9b9ec36ec6f7fb9c98deeb230869" + }, + { + "name": "princess_tone2", + "unicode": "1F478-1F3FC", + "digest": "6a0a5dc447cd887798f908c15972e7a12d28d81f168b92bcb105786ac253bea0" + }, + { + "name": "princess_tone3", + "unicode": "1F478-1F3FD", + "digest": "2f08d22fdfc7a7d66fcd87ae716b811f43077f5bb17fef87f5b7e2aa93700d70" + }, + { + "name": "princess_tone4", + "unicode": "1F478-1F3FE", + "digest": "02129211bf7bf7ff6de35913b7069aee151532d878b8c4f7e24c012e5b09d4b4" + }, + { + "name": "princess_tone5", + "unicode": "1F478-1F3FF", + "digest": "d676f103600b69dbfdb469469a77b9d561ec460ff862befa58ab30ddc909c9f7" + }, + { + "name": "printer", + "unicode": "1F5A8", + "digest": "c44402c87071f8d31d3997abab53ab9f8f7c11434e747380928814ceb6b0a417" + }, + { + "name": "prohibited", + "unicode": "1F6C7", + "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" + }, + { + "name": "projector", + "unicode": "1F4FD", + "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" + }, + { + "name": "punch", + "unicode": "1F44A", + "digest": "5759db1d7093744c74b840bbb4761fb025d6633f8fa539bcb35dcf54fc05ceb6" + }, + { + "name": "punch_tone1", + "unicode": "1F44A-1F3FB", + "digest": "793b3fa2a43c23b2c1e1b48b86ae35e8c4024cd065fac0a0a5ada87cb78d6de3" + }, + { + "name": "punch_tone2", + "unicode": "1F44A-1F3FC", + "digest": "6fc2467e99982ab00b0c352c6f7793d34faf17b16a0312082c9bd1f0709e3938" + }, + { + "name": "punch_tone3", + "unicode": "1F44A-1F3FD", + "digest": "bf747b29952550c5b4d3807b9ed85b5e5d4bcc3265b0e214791f7db547f861fb" + }, + { + "name": "punch_tone4", + "unicode": "1F44A-1F3FE", + "digest": "3b6c0ccb682552f32d6744c438e3af04a1732c67a74bcafb14c723cf526fed87" + }, + { + "name": "punch_tone5", + "unicode": "1F44A-1F3FF", + "digest": "945bae1aa3587cd1dc57d1ec4da18c67a59e0e7150dcc8735e5357b4ea1234ac" + }, + { + "name": "purple_heart", + "unicode": "1F49C", + "digest": "e0eb886e74f22d40d059ff3a089d472af53c6c53de380f428cca140dfd046345" + }, + { + "name": "purse", + "unicode": "1F45B", + "digest": "67d82ff9a4d76148b9d98538d4b786f880058a556e650ec3f93e1632aa42aaa7" + }, + { + "name": "pushpin", + "unicode": "1F4CC", + "digest": "c4de129d5d8744caffeb2f499fcc0bc6b551843938f8166ffecd0de00bda66e3" + }, + { + "name": "pushpin_black", + "unicode": "1F588", + "digest": "80ebac74edb9e8e1f8a219b32a676d318ed73b359cd8193b91b493d775307f63" + }, + { + "name": "put_litter_in_its_place", + "unicode": "1F6AE", + "digest": "b26d3b68bd62d30ecfe75cfaf309a7a0f91e92db0aa18b0b97b97baf0609d4e6" + }, + { + "name": "question", + "unicode": "2753", + "digest": "258e3169bae177fb0f01ed5f9b933f7f02dd2673e12a316af44a0c3729a78a2c" + }, + { + "name": "rabbit", + "unicode": "1F430", + "digest": "9817a7454aeda77d28f63eb13c0dc0a6d9e6c9abe3dcf538b4b3477e494cddb6" + }, + { + "name": "rabbit2", + "unicode": "1F407", + "digest": "67ba57a31b0768a2118faabdcb088f96f1441e1132397f65b6937d523ff7dabb" + }, + { + "name": "race_car", + "unicode": "1F3CE", + "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" + }, + { + "name": "racehorse", + "unicode": "1F40E", + "digest": "36aa3c7123ee7e15600657166032b21b8edeb192cf6d3ada39b5c65001f7fc40" + }, + { + "name": "radio", + "unicode": "1F4FB", + "digest": "b1403f9a883405b909208f52c9474c2d3923681ea0b02609a6e9dc12460319a5" + }, + { + "name": "radio_button", + "unicode": "1F518", + "digest": "9bcdac17b3620331a32f9bb876812231a701eb5a7f696e7d875f877ab92159fc" + }, + { + "name": "radioactive", + "unicode": "2622", + "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" + }, + { + "name": "rage", + "unicode": "1F621", + "digest": "02ac70551fc51478884c133b29539cae58b463c760db38c0aeec1bdf5b282312" + }, + { + "name": "railway_car", + "unicode": "1F683", + "digest": "8490e2ecf94c7c1d1e22fea0d80cc18a49648741009e51984f583b17bbd022e2" + }, + { + "name": "railway_track", + "unicode": "1F6E4", + "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" + }, + { + "name": "rainbow", + "unicode": "1F308", + "digest": "bbd8ecc8d0737948969a3539d2d202e599404e509f1a21bdbb0a0c41c2540522" + }, + { + "name": "raised_hand", + "unicode": "270B", + "digest": "4192881a0d613b4fcb19b1c2d8b83aadee6f0b12170721c8dd7b1ccef6540199" + }, + { + "name": "raised_hand_tone1", + "unicode": "270B-1F3FB", + "digest": "df2e046c99dceb9184c50a777b403d72bfb25ff473d6a4e20bb9a731db64ed8d" + }, + { + "name": "raised_hand_tone2", + "unicode": "270B-1F3FC", + "digest": "ed179299a1c397cd51cf6067d6795d71a3831d35e1ec9eacbf0286c8992c1e7a" + }, + { + "name": "raised_hand_tone3", + "unicode": "270B-1F3FD", + "digest": "cacbd0ddef65bc01a41bd921ea159f8cd89050309b10f15780d6199f79434a54" + }, + { + "name": "raised_hand_tone4", + "unicode": "270B-1F3FE", + "digest": "04c934c7a55b83bcfa7f3880fc1f6aa0f188090c37b9670e6775a512a1cf59e9" + }, + { + "name": "raised_hand_tone5", + "unicode": "270B-1F3FF", + "digest": "da0c4283b7b19861237c023234c6db28045b8f5a5971acb015733e08e2940e86" + }, + { + "name": "raised_hands", + "unicode": "1F64C", + "digest": "308e475f38558e73bd66e28693d77478caa5bca4360cffaffc2a97b5858c56ba" + }, + { + "name": "raised_hands_tone1", + "unicode": "1F64C-1F3FB", + "digest": "e39b9bc49dccc127e44f543e98961fcf5bcd44d6e216741bcd10ec3667263c84" + }, + { + "name": "raised_hands_tone2", + "unicode": "1F64C-1F3FC", + "digest": "f376ab13071ffdc11888ec221ef5b4de546ca0f60bd9ae30bf3da4066c220462" + }, + { + "name": "raised_hands_tone3", + "unicode": "1F64C-1F3FD", + "digest": "67694325a43e629c00fa9bd2ff7e19f84f216b2855ae2cf097762dfa7aca25e6" + }, + { + "name": "raised_hands_tone4", + "unicode": "1F64C-1F3FE", + "digest": "a2254fe75a0770708916a4ddd5db4420221c6ea9db9f74068d14eadfc0f3772c" + }, + { + "name": "raised_hands_tone5", + "unicode": "1F64C-1F3FF", + "digest": "bd7c9897cefb454ccdc46027bf56d6587565bdd345d7d0f081b7b671a53f6c99" + }, + { + "name": "raising_hand", + "unicode": "1F64B", + "digest": "d57178fc77e9fa140682634da35f9ab12a65d9b4c506b7cd8a9697f1b5910bdb" + }, + { + "name": "raising_hand_tone1", + "unicode": "1F64B-1F3FB", + "digest": "f46b34361ef79743f3187d6860182bbe1ae411031db7fe5c0f7292fa472b9c16" + }, + { + "name": "raising_hand_tone2", + "unicode": "1F64B-1F3FC", + "digest": "20b85a2ebca150b2020a04b41d34884c78c22f42c251e2b9d23fd3724574143b" + }, + { + "name": "raising_hand_tone3", + "unicode": "1F64B-1F3FD", + "digest": "5e0401b528c2b8edff766d39cdcedbe9abebe4c940df7a36ace61f59c08d508a" + }, + { + "name": "raising_hand_tone4", + "unicode": "1F64B-1F3FE", + "digest": "e4f5624264269ad09cde207cd7d4eb0fd46de816880daeec457ac8cd51cc1b7b" + }, + { + "name": "raising_hand_tone5", + "unicode": "1F64B-1F3FF", + "digest": "eb34b6c037bee5bbc4222f6aab421aa785f527ebf1b5e971769e5102244d60e1" + }, + { + "name": "ram", + "unicode": "1F40F", + "digest": "b71950d7a286a4c4909c5ec7c35211c2a5c20b6bad341bd863c6a85c4bcf9c80" + }, + { + "name": "ramen", + "unicode": "1F35C", + "digest": "7dd185b24852b577913edc78647cd53b27d42e225fde29aa2f3aba25c980b5c4" + }, + { + "name": "rat", + "unicode": "1F400", + "digest": "7a10d9ba5ee1010d421d9cf73d7966507302a69617a32fe9f1a00d57a31f7bd7" + }, + { + "name": "record_button", + "unicode": "23FA", + "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" + }, + { + "name": "recycle", + "unicode": "267B", + "digest": "74a54ed62a40dfbdcace1f08b085658a77d45c62570273927ad270bf9a8a2f4d" + }, + { + "name": "red_car", + "unicode": "1F697", + "digest": "558730d6418aa5d85b73af58c8041efd12cff906e26ea47c50963f66d33d6eb8" + }, + { + "name": "red_circle", + "unicode": "1F534", + "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307" + }, + { + "name": "registered", + "unicode": "00AE", + "digest": "ed924107384461aabb4924c401c6c087ffa047bc2ef735823e7c2be67804707c" + }, + { + "name": "relaxed", + "unicode": "263A", + "digest": "65072f7b9bfaaa92b8a0ed012dffe2cfd2efa3748264aaf450aa31ba6bd44045" + }, + { + "name": "relieved", + "unicode": "1F60C", + "digest": "1f2c7ae6a9d74a112de89403be6eca3d8155d70395e7fce51032fc961f235c7d" + }, + { + "name": "reminder_ribbon", + "unicode": "1F397", + "digest": "e4a2afc7dce40589657f7043ba8acc9638fd4117252278233ea89f84cddad387" + }, + { + "name": "repeat", + "unicode": "1F501", + "digest": "27b6dad9215e58e24c607a39dbf398ecf66ccb692c81e08eb2f5f4912db30522" + }, + { + "name": "repeat_one", + "unicode": "1F502", + "digest": "052d13f2b08eaf70b31252aa78f95d06fbe22c58945c19381b13cbeb1c855651" + }, + { + "name": "restroom", + "unicode": "1F6BB", + "digest": "b77fbc4247c241362e5ef9e6eb58b1b437aa9d16b65886cec0c55ceb55c1440e" + }, + { + "name": "revolving_hearts", + "unicode": "1F49E", + "digest": "2b8925d3e78df2dba8534252fe60bf03285346f6b3697be7668bd568e6d85931" + }, + { + "name": "rewind", + "unicode": "23EA", + "digest": "91a95b26d12ca76111556096f4d96484c9f1d7e1b20ccff5a3291b36e529a6d1" + }, + { + "name": "ribbon", + "unicode": "1F380", + "digest": "9c0296d8c2baa84c99347c431bf79b288d98b5f17b1ce7605ad7ce1da265d5aa" + }, + { + "name": "rice", + "unicode": "1F35A", + "digest": "e34849496a79e71ae4700df94f2a54895bf6de758a92edeae33fe78295a3ba21" + }, + { + "name": "rice_ball", + "unicode": "1F359", + "digest": "52df5da8b0edbdeb56d66e0f30ad4549abdd81c064f7269d920dcac66a3df2e4" + }, + { + "name": "rice_cracker", + "unicode": "1F358", + "digest": "d55f8f9d807f4619eb243c510938067a7417a64bd9435b05dfeb2a36fdb2b6a0" + }, + { + "name": "rice_scene", + "unicode": "1F391", + "digest": "482d854d8d30edfc1ecd48a4ce476e6498606321405bf5a0b4ff74489a092af8" + }, + { + "name": "right_speaker", + "unicode": "1F568", + "digest": "d268bb84be863c0884620dfc6d2a764b0c7466d2f9810549b138e21ac70add4e" + }, + { + "name": "right_speaker_one", + "unicode": "1F569", + "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" + }, + { + "name": "right_speaker_three", + "unicode": "1F56A", + "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" + }, + { + "name": "ring", + "unicode": "1F48D", + "digest": "ae2a93e7895b9b89f5a39f01d356ffed988f219ef8b658a56c55285826a4533b" + }, + { + "name": "ringing_bell", + "unicode": "1F56D", + "digest": "d71ab7fa937fc4af507b5b07ea58a4f31e875d9e8304ef2b850d7cebe0e9cd66" + }, + { + "name": "robot", + "unicode": "1F916", + "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" + }, + { + "name": "rocket", + "unicode": "1F680", + "digest": "65d8bd005ceac41904237b7a8c5f55f16713a55d971522f0bbe63a1d548e515d" + }, + { + "name": "roller_coaster", + "unicode": "1F3A2", + "digest": "907baab1f3d7becf3f8a3b1264642b395bd73b4af49e23058b3abb5c69e9106a" + }, + { + "name": "rolling_eyes", + "unicode": "1F644", + "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" + }, + { + "name": "rooster", + "unicode": "1F413", + "digest": "6cefdaa45631ed8c9480e15f578c793d95af81b42687164fd7900eee325ccf07" + }, + { + "name": "rose", + "unicode": "1F339", + "digest": "584909a4a2ece625c688f8479a39692bb8e816b692e6eb7dfd40cb045259b1b2" + }, + { + "name": "rosette", + "unicode": "1F3F5", + "digest": "0ce3b85ca05124ab99d57ebc9aa17bb246ee614d2fcda1ef62bf42ac7e616148" + }, + { + "name": "rosette_black", + "unicode": "1F3F6", + "digest": "ae8675891c88f9d98463d35178445950c39b0deb0f0e8b3f341228a6e0d0e477" + }, + { + "name": "rotating_light", + "unicode": "1F6A8", + "digest": "369e069e0bfecc7413e75f4015e9c1de527a33c7cce3f6c2b4adb60a0d9d338c" + }, + { + "name": "round_pushpin", + "unicode": "1F4CD", + "digest": "1bc5fe5a90a6e56ea00246f1b008a0e0cce0d77c226dc0300bf9a2804b543877" + }, + { + "name": "rowboat", + "unicode": "1F6A3", + "digest": "c10e09bf8be8b1a8ef3113edd9327126d6a4644f3bc81c7ada2922851e4d1cfb" + }, + { + "name": "rowboat_tone1", + "unicode": "1F6A3-1F3FB", + "digest": "a84fc1b30d1a284dcd3899dc4de8f11e7b65c258528eb41c7dbf8f82425fee12" + }, + { + "name": "rowboat_tone2", + "unicode": "1F6A3-1F3FC", + "digest": "85f001430a2ad607a15901f7c2dcf8381471f42d6cc0775e76a2ff1f457151c1" + }, + { + "name": "rowboat_tone3", + "unicode": "1F6A3-1F3FD", + "digest": "adf8b1e45a46a13f3db40c29df0312216558e9d0c615aa46a8e913cee5003a81" + }, + { + "name": "rowboat_tone4", + "unicode": "1F6A3-1F3FE", + "digest": "05482749ec40bdf02e53fc42d316c51f4f3ed643f21e8fc16b81930e4a884bda" + }, + { + "name": "rowboat_tone5", + "unicode": "1F6A3-1F3FF", + "digest": "d4bb337d948996d4a23d87f99988f02fc207815b862082ffd2eef5f0c1016aa9" + }, + { + "name": "rugby_football", + "unicode": "1F3C9", + "digest": "e14aebbded78d4a5e9b4028f79a8ca840d02798c6758cb9e926e992e2a35a4f3" + }, + { + "name": "runner", + "unicode": "1F3C3", + "digest": "58a884f06d37b0ce78197bebcd3f0e102dd90022ebd86ec70a2ef5a5cdf9683b" + }, + { + "name": "runner_tone1", + "unicode": "1F3C3-1F3FB", + "digest": "65f1633d1517803de23686d2dbcc75a5787874266db4981138ccdbe4badc773c" + }, + { + "name": "runner_tone2", + "unicode": "1F3C3-1F3FC", + "digest": "2bc81f3fb77445cdc75c34806ab0ce912bacfe47f63b5d2011a4f5d370cf7064" + }, + { + "name": "runner_tone3", + "unicode": "1F3C3-1F3FD", + "digest": "beaf5f254cba2991fdd0c38ce2ddd1b4c1110e15b2b7bc026d32f162e295c4ef" + }, + { + "name": "runner_tone4", + "unicode": "1F3C3-1F3FE", + "digest": "21d531ba9b3d13747ad636b8f7a6f184c974bf61d9f529975a64f9629263c407" + }, + { + "name": "runner_tone5", + "unicode": "1F3C3-1F3FF", + "digest": "b02a5bcc58cc45f8219262ec44c77764172fd8f2624d9122ded4a5a5db04c0ed" + }, + { + "name": "running_shirt_with_sash", + "unicode": "1F3BD", + "digest": "431bed35f4a55175bf99af769e74a81e8650c6ab34af6ecddaa1417ff7e437e6" + }, + { + "name": "sa", + "unicode": "1F202", + "digest": "a47a480631f874e8a2cd69b5d513f90a1e81a96bfa2f6025bf244a82baca3656" + }, + { + "name": "sagittarius", + "unicode": "2650", + "digest": "14871e6681c35e4a63a0b19613f77b3674d00cb78d06975e02ca29e61b5cea8c" + }, + { + "name": "sailboat", + "unicode": "26F5", + "digest": "6f742dde6c180a174b771aa3942b558e98a3dc1eb212dd31add86c5fa5620865" + }, + { + "name": "sake", + "unicode": "1F376", + "digest": "aa1392790c805950779dde7778292c937f8c1aaecb522876171d5ee542ec51f8" + }, + { + "name": "sandal", + "unicode": "1F461", + "digest": "14f1e9003a6acd90a55f23c48ed87a758fca586f2e0b0edc4dc9d1deef9eb067" + }, + { + "name": "santa", + "unicode": "1F385", + "digest": "12feddd84eb49ce30ae68d4f93d66e2c0dd11297a4d1275c9a50d4f35bea83a9" + }, + { + "name": "santa_tone1", + "unicode": "1F385-1F3FB", + "digest": "a75813770efe27d5b4c80ad892d0c796d88d1a0dbb1bd02d5f68882d7abad479" + }, + { + "name": "santa_tone2", + "unicode": "1F385-1F3FC", + "digest": "90f8072fdde5f4a275cbd1902d6c94689d453b1bee0336213dc9d6f7e1d038e1" + }, + { + "name": "santa_tone3", + "unicode": "1F385-1F3FD", + "digest": "0973053e7b77d268080126a50b95b45429630e5d49f62210e7b71840794c7dc5" + }, + { + "name": "santa_tone4", + "unicode": "1F385-1F3FE", + "digest": "5cd49c0d199a42846b400b3c1244d448ed6fe5ce993d379817cb2a5f7c0b609b" + }, + { + "name": "santa_tone5", + "unicode": "1F385-1F3FF", + "digest": "a54c36dfa99b39549fb1d3dd7f0021a7aee28112960172ed466dacc67961c525" + }, + { + "name": "satellite", + "unicode": "1F4E1", + "digest": "3b9797c8161526edce0bd8e9b8563055166f9307761c367ab3e2ad7645b6dee0" + }, + { + "name": "satellite_orbital", + "unicode": "1F6F0", + "digest": "104b135e3736a4bcfd51a42dadb53bf3e00d7f85d77a94bcb86c6704fbfacd01" + }, + { + "name": "saxophone", + "unicode": "1F3B7", + "digest": "1090da174ce8aa4f7d35025f65d5ac235e09310abde998d2a725ef3a989a2b75" + }, + { + "name": "scales", + "unicode": "2696", + "digest": "b2984caa182b691a33650344708f47c61d6d319fd067760d7594c2ef60c1e27b" + }, + { + "name": "school", + "unicode": "1F3EB", + "digest": "caf35260dc465a833521e4a0034201978fed41bbf72cd770756b3340c60e8a0c" + }, + { + "name": "school_satchel", + "unicode": "1F392", + "digest": "a89a2cc46d24d57c2d6b95ed7a56ed829ae2f97b9e6201b2d5adc78c2b78518b" + }, + { + "name": "scissors", + "unicode": "2702", + "digest": "a4e91127ac83acf5ebc64fbeca768cbbf24f2f0a484861c9c8104bee377b97ae" + }, + { + "name": "scorpion", + "unicode": "1F982", + "digest": "a090a96731bc1171b054b51abec4c9b36faa62708fd51ac48277ccf5e55d9d12" + }, + { + "name": "scorpius", + "unicode": "264F", + "digest": "1ad9bc1030a8f58f3f3223bac52c954cc7a0350805a9df7a42a26972c3b74728" + }, + { + "name": "scream", + "unicode": "1F631", + "digest": "75d613786737ee9c0a74da7394b9ae190eacc7182164627ad8205ac64e4cc09a" + }, + { + "name": "scream_cat", + "unicode": "1F640", + "digest": "eee04ff27c2c6b57d698cb87b0af8064ba8313ffc13aa090e38cd5aa8c3d2f76" + }, + { + "name": "scroll", + "unicode": "1F4DC", + "digest": "b8205847649e3ce6b946f1d1da972ed015adde3841c62971b8169235f4b41c1f" + }, + { + "name": "seat", + "unicode": "1F4BA", + "digest": "054c4db0bc8939e9dd951a3f73e9ae4b3c31652784f4d304b509c2bd32f98e31" + }, + { + "name": "secret", + "unicode": "3299", + "digest": "77daef6e5c91d55228781ddec954a7089d1851297ec81daef6e813cd22915b5e" + }, + { + "name": "see_no_evil", + "unicode": "1F648", + "digest": "aa5883fe605aeaa172d16640b8347580f9cb7d85a596da1b13955f27b0b79297" + }, + { + "name": "seedling", + "unicode": "1F331", + "digest": "a75ec929402de1e653fd6bc89e5be2f92fe5fe52f39e4b6c290eae3c59172b56" + }, + { + "name": "seven", + "unicode": "0037-20E3", + "digest": "c6a34020f6bb25871164fad44302a45c5bffced87f51dfbb816c2985ad7f6a1c" + }, + { + "name": "shamrock", + "unicode": "2618", + "digest": "530e6b987ecb9bcbf0d6e0e11bd075e7949873c784da4f9e1e1b47efd37e5058" + }, + { + "name": "shaved_ice", + "unicode": "1F367", + "digest": "fc22c3568f6be56771e83fd0e67b7eb3750041304d5d4979d3ec417f5201230e" + }, + { + "name": "sheep", + "unicode": "1F411", + "digest": "3e3656b82784164ca02c5d775db7245260f0119d2c1d35ba552a6dc75ef02544" + }, + { + "name": "shell", + "unicode": "1F41A", + "digest": "ff2f4f574b61bffd85c63bc2315c80d3cbcaba37a7c15a1f00783d312bd441d4" + }, + { + "name": "shield", + "unicode": "1F6E1", + "digest": "062aec4a325da7b637c5710846c7e7319229be49b7e59f50428442a7ef725d60" + }, + { + "name": "shinto_shrine", + "unicode": "26E9", + "digest": "9768fe94142a7dc169703d3707b203f285a546455e29fe2bbf185d44f160d6d0" + }, + { + "name": "ship", + "unicode": "1F6A2", + "digest": "f8d5b0c8ec66287b732d9171ac1913be02efb656de11501213a207d8a6c801e1" + }, + { + "name": "shirt", + "unicode": "1F455", + "digest": "e2e72c323f3bfaea02e8cf52201aa144dc56ec0f25ec97d5f04ee6c2ee99104e" + }, + { + "name": "shopping_bags", + "unicode": "1F6CD", + "digest": "0194ba540c47e4fc6403be2df68f785d56810efc2dc011dfbf700f3778cb704a" + }, + { + "name": "shower", + "unicode": "1F6BF", + "digest": "c945120182392510348de9a957c2b77a4645d118691298a2ad660dafa62a859c" + }, + { + "name": "signal_strength", + "unicode": "1F4F6", + "digest": "7876ed9d602e1be746ca0629f072d85668d1f9715e9135745e803bdf89819a3c" + }, + { + "name": "six", + "unicode": "0036-20E3", + "digest": "b409f23b73e46393c7a814442816b5880c38ef12a7feb5505e71276c195e8ca9" + }, + { + "name": "six_pointed_star", + "unicode": "1F52F", + "digest": "4bc294dcbf4185250873b52b2fb5453fb7d80df912db929add6e4b7efc066363" + }, + { + "name": "ski", + "unicode": "1F3BF", + "digest": "7ee81a2e2f7ff4e32dbf3d64b034e7542ec0c86d32e25eb125052e674943d75f" + }, + { + "name": "skier", + "unicode": "26F7", + "digest": "49df9a4206ae0c7c2dbfc8a8b13fd3e14e6f7e750bd5a8581ab6a1626d4c165e" + }, + { + "name": "skull", + "unicode": "1F480", + "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" + }, + { + "name": "skull_crossbones", + "unicode": "2620", + "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" + }, + { + "name": "sleeping", + "unicode": "1F634", + "digest": "4ead95079b1a542eedd0e5a0e93fddb318a002bdaffaa2fe5d8d7f20bf8143ed" + }, + { + "name": "sleeping_accommodation", + "unicode": "1F6CC", + "digest": "10ee8cd925a75d7977b7cf004e08b5a8147b509ee4281e879a8b57c4a7c2cb04" + }, + { + "name": "sleepy", + "unicode": "1F62A", + "digest": "dea3b246bb8af1b28e200358e3d5d59c8bba1813f35a7f4a57ec568ef43591db" + }, + { + "name": "slight_frown", + "unicode": "1F641", + "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" + }, + { + "name": "slight_smile", + "unicode": "1F642", + "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" + }, + { + "name": "slot_machine", + "unicode": "1F3B0", + "digest": "9d516b389299431b608c89d3f02ac68d28cb8df2a780f2048923bbcfbb49f416" + }, + { + "name": "small_blue_diamond", + "unicode": "1F539", + "digest": "97389e82755dc43015089dee635072357ec347f0117b2d3e9b006c46514948ee" + }, + { + "name": "small_orange_diamond", + "unicode": "1F538", + "digest": "67442d3b707501b7768f606115688373d13617ecf0b3b03ace0f1a6d38f66ddf" + }, + { + "name": "small_red_triangle", + "unicode": "1F53A", + "digest": "e0a556a3dd5bbf0290ed7c00eb6f6307dc2ea98d1fb3111fd85a7f46242a3638" + }, + { + "name": "small_red_triangle_down", + "unicode": "1F53B", + "digest": "7a11dcb8a517df220493d471759e4f4bca0db3769e2d942bbf596a88a3e57f72" + }, + { + "name": "smile", + "unicode": "1F604", + "digest": "46a7c3545b0038dfce6825d97544f6665f28512ad05c404d668e32ac599c7ecb" + }, + { + "name": "smile_cat", + "unicode": "1F638", + "digest": "c1db961f0fa261532b842816aca7ea7f6d8b461c7e930a1a1c91f96efd9db515" + }, + { + "name": "smiley", + "unicode": "1F603", + "digest": "deeaaee64ebdd9fc0bcb719db75c3f7e0c33ddbcc97f6cd51f9f84377a4368ce" + }, + { + "name": "smiley_cat", + "unicode": "1F63A", + "digest": "85ad852cb3881c4b754af172fdfc6231af42578033ea9f2981ceae944c41e72f" + }, + { + "name": "smiling_imp", + "unicode": "1F608", + "digest": "e777bdf186d89921df106d23bf002967b69afffd7e981b3cbb19f89630a06e87" + }, + { + "name": "smirk", + "unicode": "1F60F", + "digest": "2e7fddd8bed33ef4b7d8c13320302b87a28203e576ef87bd43716952cf0b5ace" + }, + { + "name": "smirk_cat", + "unicode": "1F63C", + "digest": "9ca0721f4c18592b4b809ade8f716b95fa30cd31dd87d1e41db29a319becd705" + }, + { + "name": "smoking", + "unicode": "1F6AC", + "digest": "3d14b3f0c57eb7a6a31ff371b0a454986533b79dbbeac78a76e4063478911b8d" + }, + { + "name": "snail", + "unicode": "1F40C", + "digest": "57d946c7ec84dfad71bc4f7a042927ec5712aef50c66d21af892b6c8a7faf5e1" + }, + { + "name": "snake", + "unicode": "1F40D", + "digest": "d084da540162288721364992f3b8059cbf2efd9f5b48f49a196ddbe23a073870" + }, + { + "name": "snowboarder", + "unicode": "1F3C2", + "digest": "de9e1767526de606f4908743af94cc17e89fdb0a2a44167d3d021ef09d033ab9" + }, + { + "name": "snowflake", + "unicode": "2744", + "digest": "e476863ccd7d7b549c6191fb25c121c6a467b4baef4683b7dc3e0a793c2e5d76" + }, + { + "name": "snowman", + "unicode": "26C4", + "digest": "792946b8446f2243d11b89d07c73a774be3abd36573f3918640b1ba8714270b5" + }, + { + "name": "snowman2", + "unicode": "2603", + "digest": "571acabaa4d55782c4529b762423a7e34cb1fb6bb7852cbd013e2e846d8311d1" + }, + { + "name": "sob", + "unicode": "1F62D", + "digest": "562f02ab584bcbcf9ba73cf7fa7d7129965266abd28db2c73913b8c42f2f5aca" + }, + { + "name": "soccer", + "unicode": "26BD", + "digest": "5fd0d534659b63dc862c65a80561b255bece0b76708fe8ecbae8e01b08d8cad0" + }, + { + "name": "soon", + "unicode": "1F51C", + "digest": "d2a1ab16a4056d80c827ea23f9332bb73235fc841b857cbf545062ff8aeed81d" + }, + { + "name": "sos", + "unicode": "1F198", + "digest": "fadfe8337e133a6f05d205d0807f288e5c230db04cb09f3547ce0cb73cfcf48a" + }, + { + "name": "sound", + "unicode": "1F509", + "digest": "c0074b338fd461f1f9d1143b7f9b3781ddb3fd501ea79b2410630433a8e87b83" + }, + { + "name": "space_invader", + "unicode": "1F47E", + "digest": "d264390004bd28d664dfda0069104be6db32ce477e23a95ac595bac2e29fd4e7" + }, + { + "name": "spades", + "unicode": "2660", + "digest": "d1ad99a4fc20dfea881a9062a9f2109e483dbb5dea3b29e9653cb27ec57b4800" + }, + { + "name": "spaghetti", + "unicode": "1F35D", + "digest": "ac63f9ad143e236ce6068098e5330a333ade9cddfb3dd6b1457ea47ce9dcf7e9" + }, + { + "name": "sparkle", + "unicode": "2747", + "digest": "95b8f4f1bb6080cd1d7bd333c4724dbba43ed196dce72a2bbaab46c4a1bc0e48" + }, + { + "name": "sparkler", + "unicode": "1F387", + "digest": "3a296e4d0081ad1a566e111d218e352e1439bba9fd04e8a1eb9a8e36bd438cb7" + }, + { + "name": "sparkles", + "unicode": "2728", + "digest": "5ab280ea10c30e0e0b5a26ef52b8f47ad44a983330f7ef62ac0c0888752bbdb6" + }, + { + "name": "sparkling_heart", + "unicode": "1F496", + "digest": "f145dab6b597c07e5a851176fabaf56dd857209645483d1acc1490d12c969113" + }, + { + "name": "speak_no_evil", + "unicode": "1F64A", + "digest": "6eae2d066d39c4ba81e58a8327ed875c68bc9b1297c18dc0f5243e477a81040f" + }, + { + "name": "speaker", + "unicode": "1F508", + "digest": "ea59c5a9d994808ff7937c300303e644b5f1ad41097e82f9e73ea6e1c718936c" + }, + { + "name": "speaking_head", + "unicode": "1F5E3", + "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" + }, + { + "name": "speech_balloon", + "unicode": "1F4AC", + "digest": "5dccfda46fc984583bc9eaece66e7e884f2a9eb12a69dbd3493035e3c862edd0" + }, + { + "name": "speech_left", + "unicode": "1F5E8", + "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" + }, + { + "name": "speech_right", + "unicode": "1F5E9", + "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" + }, + { + "name": "speech_three", + "unicode": "1F5EB", + "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" + }, + { + "name": "speech_two", + "unicode": "1F5EA", + "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" + }, + { + "name": "speedboat", + "unicode": "1F6A4", + "digest": "553a288ab8eeb3dee7b9d1c92eba38016caef7658beaa828136ba1d6ba8ed08a" + }, + { + "name": "spider", + "unicode": "1F577", + "digest": "519f7243b5574102ce3f8953e5480812830a1feb32ae51e8573724c864338481" + }, + { + "name": "spider_web", + "unicode": "1F578", + "digest": "42959fae08a2162d6ee8c8706f823c5932f3801bc90da30d2ca9a48c3ff25572" + }, + { + "name": "spy", + "unicode": "1F575", + "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" + }, + { + "name": "spy_tone1", + "unicode": "1F575-1F3FB", + "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" + }, + { + "name": "spy_tone2", + "unicode": "1F575-1F3FC", + "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" + }, + { + "name": "spy_tone3", + "unicode": "1F575-1F3FD", + "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" + }, + { + "name": "spy_tone4", + "unicode": "1F575-1F3FE", + "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" + }, + { + "name": "spy_tone5", + "unicode": "1F575-1F3FF", + "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" + }, + { + "name": "stadium", + "unicode": "1F3DF", + "digest": "4356db5d2cdef8c40830638debaf1f50831130c12ae8d8dc3d9a6bd28fdaa1f7" + }, + { + "name": "star", + "unicode": "2B50", + "digest": "13240b8fada84e7555892996e9f9652503bf9b9a002056c2bae428d543abe2da" + }, + { + "name": "star2", + "unicode": "1F31F", + "digest": "9b56c7548f6a222499d4e848576ea25eab837db72b207ebf8a62a451b35f758f" + }, + { + "name": "star_and_crescent", + "unicode": "262A", + "digest": "10b8a0771e415aa6610fa62185137aa1836c2bb3e82f1a3f601470e94f784923" + }, + { + "name": "star_of_david", + "unicode": "2721", + "digest": "5bc4d1038b8316281e01a9c575ded7ede0fc24c7593db5b5d36ca2e188aa5614" + }, + { + "name": "stars", + "unicode": "1F320", + "digest": "23605eafc949feead3eca145a7ff5ee3b211a8bfd95621bd35dd05df532b97c6" + }, + { + "name": "station", + "unicode": "1F689", + "digest": "c346f12fff64161041af8492550c3541a6304e53f30288224ddd0c6fe08c4d6b" + }, + { + "name": "statue_of_liberty", + "unicode": "1F5FD", + "digest": "56fa27ab059a9fd1f53aec47d9108277a3bf04a73186f36297cd1207c832ee31" + }, + { + "name": "steam_locomotive", + "unicode": "1F682", + "digest": "d0ec2eb3d761ab6157e17eab1b8b4dec3a69f9becc4251592cbb67d71825e661" + }, + { + "name": "stereo", + "unicode": "1F4FE", + "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" + }, + { + "name": "stew", + "unicode": "1F372", + "digest": "12e6e4bf48a7296700e07a053d831dd67b70c308ca9522ca96e933a4d1ef6c5e" + }, + { + "name": "stock_chart", + "unicode": "1F5E0", + "digest": "4a0fbf54d19b0b5626f91c932a24e6ac12a65b4fc276d852ff4356c8c579d28a" + }, + { + "name": "stop_button", + "unicode": "23F9", + "digest": "57310962c7738a7da4f2a62cbd5e0b26d7aec357978267a0d8ca8e6cbd7ffb02" + }, + { + "name": "stopwatch", + "unicode": "23F1", + "digest": "c8e69c24f9da98dcb41c9c6355922d08a702f12a35667fbc5beb3f659430333d" + }, + { + "name": "straight_ruler", + "unicode": "1F4CF", + "digest": "55ff7182a3696461df52e3000708083f803bc8bf0f3c25dacb34175cc104b51d" + }, + { + "name": "strawberry", + "unicode": "1F353", + "digest": "fd501e1fefb70242ac7c4dc30ad3d8c3ae200b263a832daedaa984906114afaf" + }, + { + "name": "stuck_out_tongue", + "unicode": "1F61B", + "digest": "1b49956cec511ee382177d95da77c8b6a9214a02c86bf7c6c6fd6cc9df3e9331" + }, + { + "name": "stuck_out_tongue_closed_eyes", + "unicode": "1F61D", + "digest": "60a4d5d92550c6ad4db901d42c9f6434fe94fa3ddb353b6019a93d374d9485e9" + }, + { + "name": "stuck_out_tongue_winking_eye", + "unicode": "1F61C", + "digest": "d9c15ad1c4782a0391a79aeda2745127527385b0b5fc01c8d96c3f3b637a74ae" + }, + { + "name": "sun_with_face", + "unicode": "1F31E", + "digest": "56b14e92f68f8701fdc42763e1f4695ed352845f22bd5d412f827e5cf98dd83b" + }, + { + "name": "sunflower", + "unicode": "1F33B", + "digest": "817dea222a75bb6492c32b4b144d07f48295d7dd113e21760f90b18277612ebb" + }, + { + "name": "sunglasses", + "unicode": "1F60E", + "digest": "16003cc5256397389889f52e0a5e14daea8d8c72f2ea660b8174529868cba9cd" + }, + { + "name": "sunny", + "unicode": "2600", + "digest": "f68a774b7d574fc711111e17368b57c40d973d263c7e857544a09051d4592ab9" + }, + { + "name": "sunrise", + "unicode": "1F305", + "digest": "ce06a9321bc04605538a59f9fca8536d6209d7ded03120e5d2a0be955bb17ddf" + }, + { + "name": "sunrise_over_mountains", + "unicode": "1F304", + "digest": "286244ac2bec8c5c41cf8c7c439702fa525c57fab623f7f9bd7687db0adf75b2" + }, + { + "name": "surfer", + "unicode": "1F3C4", + "digest": "d17c7ea185ca5ef5a2950ef126ee14103bf7769acb419a20d08cc023f619e459" + }, + { + "name": "surfer_tone1", + "unicode": "1F3C4-1F3FB", + "digest": "af66f2f26071b3ba8d7c795139055a58a857212f8cb1f51a507242ad7d2c49c7" + }, + { + "name": "surfer_tone2", + "unicode": "1F3C4-1F3FC", + "digest": "7a34e8b1fdad0a89bbb10333d241583ef018517fdd90f171ad7121de53776a3f" + }, + { + "name": "surfer_tone3", + "unicode": "1F3C4-1F3FD", + "digest": "b2f4cbd59a0aa93c7ee2bbb14ce55c8306dc25884377982a5f132ce6c074fa1d" + }, + { + "name": "surfer_tone4", + "unicode": "1F3C4-1F3FE", + "digest": "b16a02cfcc3606524cca9408e69c654fb83a162eaec8faae8dfd8ec67fe391c5" + }, + { + "name": "surfer_tone5", + "unicode": "1F3C4-1F3FF", + "digest": "b9a156e1aa57544b703db4e4a7773e244a3139e82c2c808c2e5a804fb524f512" + }, + { + "name": "sushi", + "unicode": "1F363", + "digest": "d2709b51ee92997c7fafa1b1517259cb896819c8dc9ba98ae26e1d44ec810d4f" + }, + { + "name": "suspension_railway", + "unicode": "1F69F", + "digest": "48903e103ef00a068b0100b28319b1e41c6a4485cb564f0ca59422ec9d3b259c" + }, + { + "name": "sweat", + "unicode": "1F613", + "digest": "8d684fa882bcbf07f4e91ea02a48cd61f22e7aa206162b8352c26fc19361ed4e" + }, + { + "name": "sweat_drops", + "unicode": "1F4A6", + "digest": "fca48e255dff08dab97ef98b75c67f7504a13be8b90afac88b69a7b7e887e445" + }, + { + "name": "sweat_smile", + "unicode": "1F605", + "digest": "0c8156554eec2396b5fee908da46484945db980d2ebc6dee57b4069a86826182" + }, + { + "name": "sweet_potato", + "unicode": "1F360", + "digest": "3ce74ea9bc14906a3d29a9592c0657aee8f7961d406992752f7580b16ca6bdd0" + }, + { + "name": "swimmer", + "unicode": "1F3CA", + "digest": "05f3aa8544e3b15837bb06ae47344633b3e60d64c572dc6638c4cee19d6e5506" + }, + { + "name": "swimmer_tone1", + "unicode": "1F3CA-1F3FB", + "digest": "85a266a9131f6a1b37e758305ca43ffb46e3e07b0a465c5faefbdb5e5adeb7a4" + }, + { + "name": "swimmer_tone2", + "unicode": "1F3CA-1F3FC", + "digest": "f2afdc4d05a2694e663a420d5ad82bd48c92aedc4137d0fd3725bf08c41bd12a" + }, + { + "name": "swimmer_tone3", + "unicode": "1F3CA-1F3FD", + "digest": "b87ecc38fb9e8eeeef8b120164d758d3f6a68a407053b03261354fd7f90f43b6" + }, + { + "name": "swimmer_tone4", + "unicode": "1F3CA-1F3FE", + "digest": "a08629cf3484953b851b357c6a04891fb97ac15e70c376bbb82af47479835e1c" + }, + { + "name": "swimmer_tone5", + "unicode": "1F3CA-1F3FF", + "digest": "21d83f66b2ef3e348f9e14ec108b9a90262d9934039ebd573471d2bdcde68974" + }, + { + "name": "symbols", + "unicode": "1F523", + "digest": "f33c3ce58374e23b8957c759016fdb5c56ef7fe812bd4e693ae8ff7574cf6bbf" + }, + { + "name": "synagogue", + "unicode": "1F54D", + "digest": "b13402c3c5793ebf924335a87a9f69befb7a6c152fc2a288261b2c2d49842eb6" + }, + { + "name": "syringe", + "unicode": "1F489", + "digest": "39e5e7530255ccf2ff35ec5c653568c8645a4711170c573117f796ea3438c44a" + }, + { + "name": "taco", + "unicode": "1F32E", + "digest": "6b004ce7129e00abcc10278bba1b9c3d5ac71888b99bf353f9878d8e494e3e0d" + }, + { + "name": "tada", + "unicode": "1F389", + "digest": "956a180a1f18e3a1252761e5b3713324f63975ee1fe32168b59b60aa4dd8b72b" + }, + { + "name": "tanabata_tree", + "unicode": "1F38B", + "digest": "d074457ba347687bfc8397ec62edee6325c411356216e7d43acd3f60628a0bb8" + }, + { + "name": "tangerine", + "unicode": "1F34A", + "digest": "1b46bb690458914220cba18c43d7ae0f6914adfee6dba7cf2bb58ed4e1854ad8" + }, + { + "name": "taurus", + "unicode": "2649", + "digest": "ea87fb3baa32605107d63b60847e4873ad9e21b7e7b652e3721cde777168670d" + }, + { + "name": "taxi", + "unicode": "1F695", + "digest": "f44249c643a96d924e1eb35f67a133f3ca61128e610a880afaa09a73c7bcaf9d" + }, + { + "name": "tea", + "unicode": "1F375", + "digest": "56ab8c291de8320c5b339e1cfbe972696e4ea31c592cefa240eda9a3abdf4fa3" + }, + { + "name": "telephone", + "unicode": "260E", + "digest": "609104588e00039199a2fef3190ee6a7be5fca7cb09b36ffe5a7d800aac69d8d" + }, + { + "name": "telephone_black", + "unicode": "1F57F", + "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" + }, + { + "name": "telephone_receiver", + "unicode": "1F4DE", + "digest": "e3bf6034de6cf2160893ba4990eba198185a6a3f9cd5767a63b048e41c297640" + }, + { + "name": "telephone_white", + "unicode": "1F57E", + "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" + }, + { + "name": "telescope", + "unicode": "1F52D", + "digest": "abe0aca5f2c78105b0e9e4c8ee7a40adcd9bb013e7c49d568076459bade73556" + }, + { + "name": "ten", + "unicode": "1F51F", + "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03" + }, + { + "name": "tennis", + "unicode": "1F3BE", + "digest": "0a5fad3f7f35da0f37761e2279c148dbe154fa14c0e2a0749209b8b2b213a388" + }, + { + "name": "tent", + "unicode": "26FA", + "digest": "7ddf437d8d186e4e3c3e818d137518d590fa06098813c7fe20e1f2a9704feab2" + }, + { + "name": "thermometer", + "unicode": "1F321", + "digest": "597d1714442698a22187fee4d57a2580322f7206c7d51e4519023824598ec08f" + }, + { + "name": "thermometer_face", + "unicode": "1F912", + "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" + }, + { + "name": "thinking", + "unicode": "1F914", + "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" + }, + { + "name": "thought_balloon", + "unicode": "1F4AD", + "digest": "76c8513191641f0a79e878ccc0d83c4576984609810633f596db2f64cc684b7d" + }, + { + "name": "thought_left", + "unicode": "1F5EC", + "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" + }, + { + "name": "thought_right", + "unicode": "1F5ED", + "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" + }, + { + "name": "three", + "unicode": "0033-20E3", + "digest": "ca0147a8f67cea3bc2516fa8deef4325188359559786c94ff0b27f90eef04b88" + }, + { + "name": "thumbs_down_reverse", + "unicode": "1F593", + "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" + }, + { + "name": "thumbs_up_reverse", + "unicode": "1F592", + "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" + }, + { + "name": "thumbsdown", + "unicode": "1F44E", + "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" + }, + { + "name": "thumbsdown_tone1", + "unicode": "1F44E-1F3FB", + "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" + }, + { + "name": "thumbsdown_tone2", + "unicode": "1F44E-1F3FC", + "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" + }, + { + "name": "thumbsdown_tone3", + "unicode": "1F44E-1F3FD", + "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" + }, + { + "name": "thumbsdown_tone4", + "unicode": "1F44E-1F3FE", + "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" + }, + { + "name": "thumbsdown_tone5", + "unicode": "1F44E-1F3FF", + "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" + }, + { + "name": "thumbsup", + "unicode": "1F44D", + "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" + }, + { + "name": "thumbsup_tone1", + "unicode": "1F44D-1F3FB", + "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" + }, + { + "name": "thumbsup_tone2", + "unicode": "1F44D-1F3FC", + "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" + }, + { + "name": "thumbsup_tone3", + "unicode": "1F44D-1F3FD", + "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" + }, + { + "name": "thumbsup_tone4", + "unicode": "1F44D-1F3FE", + "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" + }, + { + "name": "thumbsup_tone5", + "unicode": "1F44D-1F3FF", + "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" + }, + { + "name": "thunder_cloud_rain", + "unicode": "26C8", + "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" + }, + { + "name": "ticket", + "unicode": "1F3AB", + "digest": "a7654a5529535120da3c377e72cd1f7997bdc2dabf1d44b584f7df7852b158f9" + }, + { + "name": "tickets", + "unicode": "1F39F", + "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" + }, + { + "name": "tiger", + "unicode": "1F42F", + "digest": "9ebe3117f5f1b589ff8164f8d87dcc275923e0db87121d2cee0fdb9b56dfc4ac" + }, + { + "name": "tiger2", + "unicode": "1F405", + "digest": "212c95dc60d52420a6320917fe3fdd0683b4edc1a2a2c4a1c60920d1f90f4bc3" + }, + { + "name": "timer", + "unicode": "23F2", + "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" + }, + { + "name": "tired_face", + "unicode": "1F62B", + "digest": "ad687a956388ec53ca1e301a0abe2f1e2cfb9f73cd543dd61a21c7335a42e332" + }, + { + "name": "tm", + "unicode": "2122", + "digest": "1156c8b0af40b336bbb6534b3302ac63eab009c4cd0476adcf1fc4669f04b647" + }, + { + "name": "toilet", + "unicode": "1F6BD", + "digest": "a4a24529c21e00e0861f4160c771f0e90aae8f6aee7550ad30d3dbb3fabbd4be" + }, + { + "name": "tokyo_tower", + "unicode": "1F5FC", + "digest": "6324f154f5f5c722044129e5bca03484aca1439911585e42c1c181ffa30b480c" + }, + { + "name": "tomato", + "unicode": "1F345", + "digest": "41bb6de095b27815eacb74a70aea8f7d4fe1ff947182b112001dd47ae7e45fbb" + }, + { + "name": "tone1", + "unicode": "1F3FB", + "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c" + }, + { + "name": "tone2", + "unicode": "1F3FC", + "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f" + }, + { + "name": "tone3", + "unicode": "1F3FD", + "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8" + }, + { + "name": "tone4", + "unicode": "1F3FE", + "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3" + }, + { + "name": "tone5", + "unicode": "1F3FF", + "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81" + }, + { + "name": "tongue", + "unicode": "1F445", + "digest": "bf9dd7c65a8dc5d77eb013658a0a12a13f7b224a784e65e203d9584bb6b41427" + }, + { + "name": "tools", + "unicode": "1F6E0", + "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" + }, + { + "name": "top", + "unicode": "1F51D", + "digest": "d645030099aeb433307569e8e1c4342c1c411a8fefe50fdca7a3207a1a0db671" + }, + { + "name": "tophat", + "unicode": "1F3A9", + "digest": "1082fb2ee2e98fe65d21081b74ca59b07adef85043e2d36f25cac69db2d31fd3" + }, + { + "name": "track_next", + "unicode": "23ED", + "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" + }, + { + "name": "track_previous", + "unicode": "23EE", + "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" + }, + { + "name": "trackball", + "unicode": "1F5B2", + "digest": "8332503454ce42059d720c285fe2b15eb0562a0a4b234dccb0f3159bb30a91aa" + }, + { + "name": "tractor", + "unicode": "1F69C", + "digest": "a41d304c41a85d966f6a7c301735fdbe2ae41f4471dd7dcd72023046ca2546d0" + }, + { + "name": "traffic_light", + "unicode": "1F6A5", + "digest": "005f68d028fec8d9ae389cc2b23e1343a82c028eb32820d5e56f5c84eba315d1" + }, + { + "name": "train", + "unicode": "1F68B", + "digest": "bf32893b7b9ecd248e8afe840624061746ac6ceb741e3e861ebfa46014f4bed4" + }, + { + "name": "train2", + "unicode": "1F686", + "digest": "08a9732453a0b4f68dd2d3d3879f04ee538f65897913b5a5157c0585132a374a" + }, + { + "name": "train_diesel", + "unicode": "1F6F2", + "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" + }, + { + "name": "tram", + "unicode": "1F68A", + "digest": "5a86d31f7ab677d967fecd75babc900b5169766d0228961912314c4c4d1d64ee" + }, + { + "name": "triangle_round", + "unicode": "1F6C6", + "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" + }, + { + "name": "triangular_flag_on_post", + "unicode": "1F6A9", + "digest": "d824c973d84cd62c845d64e546de87b094fda8f9972b6a33acd75e1a5ac19f75" + }, + { + "name": "triangular_ruler", + "unicode": "1F4D0", + "digest": "5576802d8bcb8836f473d9c7641ff666250c23c8476c676b253e577695025959" + }, + { + "name": "trident", + "unicode": "1F531", + "digest": "70c1e8254da5b0e4552673b487503a20feeb249484d4596836b75de70220be82" + }, + { + "name": "triumph", + "unicode": "1F624", + "digest": "b09262121b0d3d9d017ded22d0fbb1acaa6ee8c9d38e9ac34292b390d97408fe" + }, + { + "name": "trolleybus", + "unicode": "1F68E", + "digest": "5af943836cc30c3b79160c70b6488c984fa63c104dce08c436597a93d30ff6f4" + }, + { + "name": "trophy", + "unicode": "1F3C6", + "digest": "c249938815042716db2b39cdece6715fabf9e56ed583270c451925e6c91f9191" + }, + { + "name": "tropical_drink", + "unicode": "1F379", + "digest": "352d903e813a27d2a74803322539b50a50aec0ca2ed7ab4a92ec480b1c226cb6" + }, + { + "name": "tropical_fish", + "unicode": "1F420", + "digest": "13a104ca9c326238ab8d85b60759629b4efaa836946fbe58d78d779443475f7b" + }, + { + "name": "truck", + "unicode": "1F69A", + "digest": "13d381d6b43b42350a1e24c02296904b8fdc38c1bf0939fc7037850127e91f21" + }, + { + "name": "trumpet", + "unicode": "1F3BA", + "digest": "df7fb48920ac0919ee2d7b30102016479f747a5d4dd25b3e18d9f17121d232d1" + }, + { + "name": "tulip", + "unicode": "1F337", + "digest": "519a84336464b5dc8db57eecef3e5b8ed82ccfdaa0ed0fa9ef7bcf0e8acea1f8" + }, + { + "name": "turkey", + "unicode": "1F983", + "digest": "e87bff52ad3e301dc62f6832b8a6fcaf99db260a96263e4203a55ce3abda8cf8" + }, + { + "name": "turned_ok_hand", + "unicode": "1F58F", + "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" + }, + { + "name": "turtle", + "unicode": "1F422", + "digest": "388b3e75b931638a09f65b842d26e2cc87b200ba782dec871f84cddd71aaeaf3" + }, + { + "name": "tv", + "unicode": "1F4FA", + "digest": "dba03be6482d6291599c7393b0f749c0de5c873d45c96a20ccc53b3e104a6a24" + }, + { + "name": "twisted_rightwards_arrows", + "unicode": "1F500", + "digest": "5fcad0247576e10e683f353008749975e9371a4f66c0901a73c3a0c7803c63c7" + }, + { + "name": "two", + "unicode": "0032-20E3", + "digest": "20ad722532a5073fff8aef0a5e890421da0ae97f0723a8a2cc503c13d24ba597" + }, + { + "name": "two_hearts", + "unicode": "1F495", + "digest": "160cb11e3ed2ae1b20957d445c6c4b4bd604d067294818dfeeefba4562425eb9" + }, + { + "name": "two_men_holding_hands", + "unicode": "1F46C", + "digest": "923734704e544f7484fdb424bfe26f51ee07754db712cd151f8fbe955023a1ab" + }, + { + "name": "two_women_holding_hands", + "unicode": "1F46D", + "digest": "58a40e7819cab3589ac81bb4fdc485b7196ee355544b54c6b00169028c260130" + }, + { + "name": "u5272", + "unicode": "1F239", + "digest": "b7e8ad52629a1f1fca77a5c9a51da87ce2b9a81f6af9bcbe9bec9552d398e9bf" + }, + { + "name": "u5408", + "unicode": "1F234", + "digest": "f359799d206cff6aae3af26eb8ad153abd38e817d4c70b2e5e5e8cf2f46e645e" + }, + { + "name": "u55b6", + "unicode": "1F23A", + "digest": "c40293bea0f148e76ca5152e830b1b474380fe259180fbf74fece1ccc9afd8a3" + }, + { + "name": "u6307", + "unicode": "1F22F", + "digest": "45449f7ae29da9e507c19d0f2b22f17f7cbd763f2ec87eb893be5bae49c7f78e" + }, + { + "name": "u6708", + "unicode": "1F237", + "digest": "b897ead8c952013975ce6f381cdb8c584ebe4015311ef87f2a332c8a9e155d75" + }, + { + "name": "u6709", + "unicode": "1F236", + "digest": "8b2f792abc1313a1a58f2fb8b37ad68a964004c962535f7739131257b1331a05" + }, + { + "name": "u6e80", + "unicode": "1F235", + "digest": "fd982a56d4c492e63526b427bb948d7f155b0d5c414a68c7177698a71e72269b" + }, + { + "name": "u7121", + "unicode": "1F21A", + "digest": "334f87a5254b58503d9f7a8ecc3d971a99839ec9c22c443469d72caca1750a48" + }, + { + "name": "u7533", + "unicode": "1F238", + "digest": "3c8e743ae9960e43b9fa0cc698018fcb2a52ae34d143f0561298191f9def019c" + }, + { + "name": "u7981", + "unicode": "1F232", + "digest": "a08bf39be3a54c076de79478c09b79c5c4d221853722870dd6e81abb78a4b64a" + }, + { + "name": "u7a7a", + "unicode": "1F233", + "digest": "5dfb74a534a6490df989f84eac271c79d52f29313b6d43662dd0ff029794367c" + }, + { + "name": "umbrella", + "unicode": "2614", + "digest": "ff1191f6c11b82f5337f78aadb58af50c69abaf676a384b0473bf49004e4018f" + }, + { + "name": "umbrella2", + "unicode": "2602", + "digest": "aa7db9d6ed42dff847a8e5ee48a8eeff7a6e7f30de155a28951407f5aaa3dae2" + }, + { + "name": "unamused", + "unicode": "1F612", + "digest": "efbbcaee6f3178afe509d74d13243ec6befe3112620a01e5079171eac4b32417" + }, + { + "name": "underage", + "unicode": "1F51E", + "digest": "ae9a300fa400a57b7216a0a040fb8a5f02236fbceeeceed58bfd953c87ad51fe" + }, + { + "name": "unicorn", + "unicode": "1F984", + "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" + }, + { + "name": "unlock", + "unicode": "1F513", + "digest": "63dbef0855399254ae01cf4ef0676adebc1432ae1ee260b569c23ae8152deaf8" + }, + { + "name": "up", + "unicode": "1F199", + "digest": "902a3ecbcd73099a28476b49bc9e7b06da6cc002ee584e0501e5b625fb515088" + }, + { + "name": "upside_down", + "unicode": "1F643", + "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" + }, + { + "name": "urn", + "unicode": "26B1", + "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" + }, + { + "name": "v", + "unicode": "270C", + "digest": "df85ad1a3ff365c3232a010701c9b25cd824d19fa2511422dee60ac231f457e3" + }, + { + "name": "v_tone1", + "unicode": "270C-1F3FB", + "digest": "ce45db8de862b6f37d9208920d7c7c19335fac2cbff59b52be1ccbc01e3249da" + }, + { + "name": "v_tone2", + "unicode": "270C-1F3FC", + "digest": "9036c8d793b02b4d2e6a4752b8ec319ec50efd6fcd6feef7b0671a63e5659acc" + }, + { + "name": "v_tone3", + "unicode": "270C-1F3FD", + "digest": "a94b95f7656d62b442c99f2643b96b0c6114683401a94cdda68405c37efecc4c" + }, + { + "name": "v_tone4", + "unicode": "270C-1F3FE", + "digest": "5c75f74993856f2faeeaee68df7689056e60d30e8c573039db8303167f7d0a80" + }, + { + "name": "v_tone5", + "unicode": "270C-1F3FF", + "digest": "bb899672adb3c11f65983fbf9581de7f0a1bbac86fde146e799cea1126fe241e" + }, + { + "name": "vertical_traffic_light", + "unicode": "1F6A6", + "digest": "36296e03620f16d35e5cec195cd97f5b358dfdedcd43bc1b3f7988ff7e85ab47" + }, + { + "name": "vhs", + "unicode": "1F4FC", + "digest": "f4be55f4c23a85e0caacbf569742c117c8fd52c189465a6560cbd2f8873ad74f" + }, + { + "name": "vibration_mode", + "unicode": "1F4F3", + "digest": "b9b8dfa3160c22f78b7d627cb52636d81ca6230a196cee5e94028e32e06b9a98" + }, + { + "name": "video_camera", + "unicode": "1F4F9", + "digest": "3bfaa24e5fb00145e3e4dd07ecf569dabbb3f211551e46085ef23cf23002cfc3" + }, + { + "name": "video_game", + "unicode": "1F3AE", + "digest": "4dcbd76030e37d0f7429852991a5f3f126cbdedfc124ecad0ba29d227375f6e2" + }, + { + "name": "violin", + "unicode": "1F3BB", + "digest": "8ab7adc6e1e934f9e05009cd0a6d4da3136092c8f11c0606b91914be182206f5" + }, + { + "name": "virgo", + "unicode": "264D", + "digest": "aaa19752756d0cac949445de1d2b8bf1f75a071368ae0acf5002f4acdc34826f" + }, + { + "name": "volcano", + "unicode": "1F30B", + "digest": "86c17d61d66bfa868c02f1d31daca22f077c096368ef53cd9bfb9914a2f0b273" + }, + { + "name": "volleyball", + "unicode": "1F3D0", + "digest": "b505684b13f814fbc08dc8ff652849328f46068276e0a24ae1961e2aff15868f" + }, + { + "name": "vs", + "unicode": "1F19A", + "digest": "e31bd8b48b88c21d717964d1360a7751684dd1e0b63fdd655f1a9ec10a952dfb" + }, + { + "name": "vulcan", + "unicode": "1F596", + "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" + }, + { + "name": "vulcan_tone1", + "unicode": "1F596-1F3FB", + "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" + }, + { + "name": "vulcan_tone2", + "unicode": "1F596-1F3FC", + "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" + }, + { + "name": "vulcan_tone3", + "unicode": "1F596-1F3FD", + "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" + }, + { + "name": "vulcan_tone4", + "unicode": "1F596-1F3FE", + "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" + }, + { + "name": "vulcan_tone5", + "unicode": "1F596-1F3FF", + "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" + }, + { + "name": "walking", + "unicode": "1F6B6", + "digest": "8ec0b2207d4368422261bc58944c17dff2554b2356becfb18f21dd87425cd67b" + }, + { + "name": "walking_tone1", + "unicode": "1F6B6-1F3FB", + "digest": "9ee2224226326833fb0c9598c737fbd2f6bca1c81f082537e9f22ea1de4ff48e" + }, + { + "name": "walking_tone2", + "unicode": "1F6B6-1F3FC", + "digest": "4855d521e937d10d58eeb2bbada493699e31e1098128f81a9e3303bcf3edeb49" + }, + { + "name": "walking_tone3", + "unicode": "1F6B6-1F3FD", + "digest": "82669cf7167054a3615add01059f87dbb809edac3889ee171d5994de90448000" + }, + { + "name": "walking_tone4", + "unicode": "1F6B6-1F3FE", + "digest": "c11f03aa96248272f831f68b93c5b21b2ecbffeb1b4c1c13373bf539ee7db8f8" + }, + { + "name": "walking_tone5", + "unicode": "1F6B6-1F3FF", + "digest": "18238ee121a64211f6bcdbd475cee4ad6debe2bf421daba53d125aa005c26d10" + }, + { + "name": "waning_crescent_moon", + "unicode": "1F318", + "digest": "96ef03ff85247877255a5ca3e8a8bb63f7d41f66531e8db61cbcd863e3ad7355" + }, + { + "name": "waning_gibbous_moon", + "unicode": "1F316", + "digest": "994223113ad151e6b42ee317a10dad18f86759a308e61ab88eeb10ab780aae67" + }, + { + "name": "warning", + "unicode": "26A0", + "digest": "a702e51efd1a3ab425eada008ccf694f38a71db14bb710edacc2e206d61f5ca3" + }, + { + "name": "wastebasket", + "unicode": "1F5D1", + "digest": "afecb31aaf5078298ab9f7c5da29a49ce0cdefe477ee50889be9c0e43ccf1799" + }, + { + "name": "watch", + "unicode": "231A", + "digest": "410334c87b8552f601f4ea1b7e36582a8b22f11b804d5ab1008d4af2b5a0cbe6" + }, + { + "name": "water_buffalo", + "unicode": "1F403", + "digest": "d1becfaea464372c46e5442c6030ea355806ce5864c2435c123a9bb3a2c3c5eb" + }, + { + "name": "watermelon", + "unicode": "1F349", + "digest": "88dd78812520c44080c79fe8cb1825bc713e5155da2ce8c73286333749e7035e" + }, + { + "name": "wave", + "unicode": "1F44B", + "digest": "5103c49914ff1a2d76a1ab6db2530ddd9f48b98b708ab15292ceadf28873c939" + }, + { + "name": "wave_tone1", + "unicode": "1F44B-1F3FB", + "digest": "ef2d79f377d09dedd1e900b2f4e4a2412bf562cd88484f71c52d465053f8aae9" + }, + { + "name": "wave_tone2", + "unicode": "1F44B-1F3FC", + "digest": "d323e6e2e9ce035bc11b98226d46ab393dfdf3909d99e7a828b51950e6574656" + }, + { + "name": "wave_tone3", + "unicode": "1F44B-1F3FD", + "digest": "8a8a386d53252455c20d6b235c462fd9cb3b20c9c19c67e67b3dece4621b5cf6" + }, + { + "name": "wave_tone4", + "unicode": "1F44B-1F3FE", + "digest": "a8281c2ab9cf6e2b3d3cad24707fe412ec2398195530b716a2617477416c0432" + }, + { + "name": "wave_tone5", + "unicode": "1F44B-1F3FF", + "digest": "5ccbee95bfc180580c8a02b88146110c4d132b8ea618dd6a58f03c1db921d58d" + }, + { + "name": "wavy_dash", + "unicode": "3030", + "digest": "b5b67fc12938801a98ff22b6f7b566c603f58c183737fa740a500724879f0e99" + }, + { + "name": "waxing_crescent_moon", + "unicode": "1F312", + "digest": "20446122d170b18f88ea71524f6747d42b97f9d765c52e676e5163fee58ec379" + }, + { + "name": "waxing_gibbous_moon", + "unicode": "1F314", + "digest": "4324e43d4d45e6333f7379c9feb8efd3093d76f3920d7dc5ad3c615e76104998" + }, + { + "name": "wc", + "unicode": "1F6BE", + "digest": "cb7c5d35bf11149d12cda2c0897cb6038e043127055bbe2e8e33c9b422d6d8fc" + }, + { + "name": "weary", + "unicode": "1F629", + "digest": "29a291033a1b67eda3710dffae42d63fcfa663e37dab728c236172f3e877fe8f" + }, + { + "name": "wedding", + "unicode": "1F492", + "digest": "6c7d874f464c9c76b0d767135aa40ced94089b5f71d373098b47488d7f3ef7c4" + }, + { + "name": "whale", + "unicode": "1F433", + "digest": "94168acda6ba502b64ea50ff4aaafb7e6258d7c6806e91f090c8a3c46edc5b6d" + }, + { + "name": "whale2", + "unicode": "1F40B", + "digest": "e1cde2308bd510b2449c96e88ffec796856f98b19ceedc1cd7e9ea009dae1417" + }, + { + "name": "wheel_of_dharma", + "unicode": "2638", + "digest": "bbd6927697c22a1c3e56fd0c9933d9e00dbf120505fe48d02cb486bcd67a8b2c" + }, + { + "name": "wheelchair", + "unicode": "267F", + "digest": "513f759acf528f6a7e39d9de1d171c3faebe645c9cf3bd86b185123016beef95" + }, + { + "name": "white_check_mark", + "unicode": "2705", + "digest": "a0b3bf7c4fb131e7a9fab5169ea4094e2665e02cedaa091f0d6e78609b2f17ed" + }, + { + "name": "white_circle", + "unicode": "26AA", + "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c" + }, + { + "name": "white_flower", + "unicode": "1F4AE", + "digest": "a3efea4950e09994f5e9d3d16f0728969238302304a6cce90b293c56e9a3e20c" + }, + { + "name": "white_large_square", + "unicode": "2B1C", + "digest": "99c4442a65f2e3c568f45aed9e74590206c517a716557f4d741d967c9f42ed40" + }, + { + "name": "white_medium_small_square", + "unicode": "25FD", + "digest": "a1edfeb4e540dcc020ba5dde19f7a18d90966788baa5382a22a0f9038d593f01" + }, + { + "name": "white_medium_square", + "unicode": "25FB", + "digest": "794c2339ca71bb6d65ac488fb7b5dc4f0a2412f30890d2c4ece53cdbf52ba78b" + }, + { + "name": "white_small_square", + "unicode": "25AB", + "digest": "9c4c308070a0c4524993cc36feaa778aad8f0df9f209b82d28b1f3811c441bc4" + }, + { + "name": "white_square_button", + "unicode": "1F533", + "digest": "f46e18c7250c874d1b4d6117eda741d86a081352e76f3d019dd64af2669fa4bb" + }, + { + "name": "white_sun_cloud", + "unicode": "1F325", + "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" + }, + { + "name": "white_sun_rain_cloud", + "unicode": "1F326", + "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" + }, + { + "name": "white_sun_small_cloud", + "unicode": "1F324", + "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" + }, + { + "name": "wind_blowing_face", + "unicode": "1F32C", + "digest": "20bdeb8e39dc637792ac9fbee031c5791889f3126e83556ba51f98809c19763c" + }, + { + "name": "wind_chime", + "unicode": "1F390", + "digest": "1fc26f33ce13b6a969bb76e914de054ec5d1c7c4cd1dc5ee8fea5f3149f794d8" + }, + { + "name": "wine_glass", + "unicode": "1F377", + "digest": "7dfcf9c5195a20fd2745b19e102910392b0fc8f1650b98ab81957807841935e0" + }, + { + "name": "wink", + "unicode": "1F609", + "digest": "404ac6c920414ca35894da1d97b3b2fabe92bd09569274eb5798fbb297129036" + }, + { + "name": "wolf", + "unicode": "1F43A", + "digest": "ebadd7766c4a314b4027c32435a2f5727a6283123dfb8834e10251cbfc07ca2f" + }, + { + "name": "woman", + "unicode": "1F469", + "digest": "9f0dbb5d1e0db4f008141582dcb6413f5aebaa13e191349c976a435b2bee0956" + }, + { + "name": "woman_tone1", + "unicode": "1F469-1F3FB", + "digest": "c1f2a503481fdd96cfbfa7d556500f8e0da0cea1c72ed1078ecbb6962221c22a" + }, + { + "name": "woman_tone2", + "unicode": "1F469-1F3FC", + "digest": "bf78b3a8f7424037069f8ac337e154ef185f55026c71a6cf6dbe15eb42ef9813" + }, + { + "name": "woman_tone3", + "unicode": "1F469-1F3FD", + "digest": "4ccd70a2052b932b3395ac0a957c05815327dc8082fd461abcd797411db8ce05" + }, + { + "name": "woman_tone4", + "unicode": "1F469-1F3FE", + "digest": "71b5efc4a410102e60048ca05f87587384a6db309f3be94109a4f92ea97072dc" + }, + { + "name": "woman_tone5", + "unicode": "1F469-1F3FF", + "digest": "91a1cd015731f4db501c276a8236eb0665e4dc7aa1891e2a67b8d3e543fbea9c" + }, + { + "name": "womans_clothes", + "unicode": "1F45A", + "digest": "599332c0b863a40fd0c319e4e0f52ae847326a96d180c288e0466b3ac308a27e" + }, + { + "name": "womans_hat", + "unicode": "1F452", + "digest": "231ff55c3fa56d8fb5731fe41f547e67ffacfdde82286f45d4ca65a2d2821239" + }, + { + "name": "womens", + "unicode": "1F6BA", + "digest": "f971429456b543804412490af2e27e0b14d0d536a156db898bce67b136e1b563" + }, + { + "name": "worried", + "unicode": "1F61F", + "digest": "e017f636e79b9301f3a06471a5f3513ba7dbb9b97938de1140c1df4c32fd8844" + }, + { + "name": "wrench", + "unicode": "1F527", + "digest": "c9ded4f7f496bad8691677226310bbd31bb485722ea479bc7a68a2b4ef9d55d9" + }, + { + "name": "writing_hand", + "unicode": "1F58E", + "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" + }, + { + "name": "writing_hand_tone1", + "unicode": "270D-1F3FB", + "digest": "38e64e6dca4847a12aef8a117c113b2025d841501c4bc8188c57d0c8a4f1e34d" + }, + { + "name": "writing_hand_tone2", + "unicode": "270D-1F3FC", + "digest": "2b2d0ac2701ae707c31d9c85feb2e3700e11398701e2b0519338897817d53baf" + }, + { + "name": "writing_hand_tone3", + "unicode": "270D-1F3FD", + "digest": "85d67f90ff8bd2e7157f28fd857e6730b660a7eb82eb5350f57671f728ce725b" + }, + { + "name": "writing_hand_tone4", + "unicode": "270D-1F3FE", + "digest": "056c05c201b3d0972433f00910967ad7334e37726e2956fee053ec2e1a9153c7" + }, + { + "name": "writing_hand_tone5", + "unicode": "270D-1F3FF", + "digest": "95c59157d301ee08990e4302fd9bdd7953e1d1abed09636d0837d84e44f53ba6" + }, + { + "name": "x", + "unicode": "274C", + "digest": "1d256b0015b9cbdeaa4558f9241782c89d86c79a42e507621f7949c56a90b6c0" + }, + { + "name": "yellow_heart", + "unicode": "1F49B", + "digest": "e869a80266b4379a8d82988fef25e187632bfb076ae619f576e416906cd688a7" + }, + { + "name": "yen", + "unicode": "1F4B4", + "digest": "8f3d801c687e585e4497123c5c91a8b0c558578deec6a8c1591b25e64a3a8992" + }, + { + "name": "yin_yang", + "unicode": "262F", + "digest": "e8ea4c686518ad6165e15ed67b529f2f1e20d648aa2ecb7e9bff5a6067dd3fea" + }, + { + "name": "yum", + "unicode": "1F60B", + "digest": "d9c97bbf6bdb6e39977437680f0b37c9335306c51e01114056ae1d4c9c85b0e0" + }, + { + "name": "zap", + "unicode": "26A1", + "digest": "37588734c7fe330ae35e6ee99e7cf4183e8fe1bc01f6bbbc6293b21076a338cb" + }, + { + "name": "zero", + "unicode": "0030-20E3", + "digest": "519c927db8264d5379ab2c6a18656ea6dd1ceb2afc92eb48563bf86af4697571" + }, + { + "name": "zipper_mouth", + "unicode": "1F910", + "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" + }, + { + "name": "zzz", + "unicode": "1F4A4", + "digest": "f07c56d2d55c0a886c26a8e3d49a9adeab54cc1a0c0354ea8d3bf23aaed3176d" + } +] \ No newline at end of file diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 592100a704..231840148d 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -64,7 +64,7 @@ module API authorize_admin_project @branch = user_project.repository.find_branch(params[:branch]) - not_found!("Branch does not exist") unless @branch + not_found!("Branch") unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch.destroy if protected_branch diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 197e826e5b..939469b388 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -8,14 +8,14 @@ module API expose :id, :state, :avatar_url expose :web_url do |user, options| - Gitlab::Application.routes.url_helpers.user_url(user) + Gitlab::Routing.url_helpers.user_url(user) end end class User < UserBasic expose :created_at expose :is_admin?, as: :is_admin - expose :bio, :skype, :linkedin, :twitter, :website_url + expose :bio, :location, :skype, :linkedin, :twitter, :website_url end class Identity < Grape::Entity @@ -89,7 +89,7 @@ module API expose :avatar_url expose :web_url do |group, options| - Gitlab::Application.routes.url_helpers.group_url(group) + Gitlab::Routing.url_helpers.group_url(group) end end @@ -170,6 +170,10 @@ module API expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic + + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user]) + end end class MergeRequest < ProjectEntity @@ -183,6 +187,10 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status + + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user]) + end end class MergeRequestChanges < MergeRequest @@ -204,7 +212,7 @@ module API expose :note, as: :body expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic - expose :created_at + expose :created_at, :updated_at expose :system?, as: :system expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false @@ -292,7 +300,8 @@ module API end class Label < Grape::Entity - expose :name, :color + expose :name, :color, :description + expose :open_issues_count, :closed_issues_count, :open_merge_requests_count end class Compare < Grape::Entity @@ -334,7 +343,6 @@ module API expose :updated_at expose :home_page_url expose :default_branch_protection - expose :twitter_sharing_enabled expose :restricted_visibility_levels expose :max_attachment_size expose :session_expire_delay diff --git a/lib/api/issues.rb b/lib/api/issues.rb index e5ae88eb96..c4ea05ee6c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -55,7 +55,7 @@ module API issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues.reorder(issuable_order_by => issuable_sort) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -92,7 +92,7 @@ module API end issues.reorder(issuable_order_by => issuable_sort) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user end # Get a single project issue @@ -105,23 +105,27 @@ module API get ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) not_found! unless can?(current_user, :read_issue, @issue) - present @issue, with: Entities::Issue + present @issue, with: Entities::Issue, current_user: current_user end # Create a new project issue # # Parameters: - # id (required) - The ID of a project - # title (required) - The title of an issue - # description (optional) - The description of an issue - # assignee_id (optional) - The ID of a user to assign issue + # id (required) - The ID of a project + # title (required) - The title of an issue + # description (optional) - The description of an issue + # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue - # labels (optional) - The labels of an issue + # labels (optional) - The labels of an issue + # created_at (optional) - The date # Example Request: # POST /projects/:id/issues post ":id/issues" do required_attributes! [:title] - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] + + keys = [:title, :description, :assignee_id, :milestone_id] + keys << :created_at if current_user.admin? || user_project.owner == current_user + attrs = attributes_for_keys(keys) # Validate label names in advance if (errors = validate_label_params(params)).any? @@ -145,7 +149,7 @@ module API issue.add_labels_by_names(params[:labels].split(',')) end - present issue, with: Entities::Issue + present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end @@ -185,7 +189,7 @@ module API issue.add_labels_by_names(params[:labels].split(',')) end - present issue, with: Entities::Issue + present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 78ca58ad0d..4af6bef0fa 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -17,17 +17,18 @@ module API # Creates a new label # # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the label to be deleted - # color (required) - Color of the label given in 6-digit hex - # notation with leading '#' sign (e.g. #FFAABB) + # id (required) - The ID of a project + # name (required) - The name of the label to be created + # color (required) - Color of the label given in 6-digit hex + # notation with leading '#' sign (e.g. #FFAABB) + # description (optional) - The description of label to be created # Example Request: # POST /projects/:id/labels post ':id/labels' do authorize! :admin_label, user_project required_attributes! [:name, :color] - attrs = attributes_for_keys [:name, :color] + attrs = attributes_for_keys [:name, :color, :description] label = user_project.find_label(attrs[:name]) conflict!('Label already exists') if label @@ -62,11 +63,12 @@ module API # Updates an existing label. At least one optional parameter is required. # # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the label to be deleted - # new_name (optional) - The new name of the label - # color (optional) - Color of the label given in 6-digit hex - # notation with leading '#' sign (e.g. #FFAABB) + # id (required) - The ID of a project + # name (required) - The name of the label to be deleted + # new_name (optional) - The new name of the label + # color (optional) - Color of the label given in 6-digit hex + # notation with leading '#' sign (e.g. #FFAABB) + # description (optional) - The description of label to be created # Example Request: # PUT /projects/:id/labels put ':id/labels' do @@ -76,7 +78,7 @@ module API label = user_project.find_label(params[:name]) not_found!('Label not found') unless label - attrs = attributes_for_keys [:new_name, :color] + attrs = attributes_for_keys [:new_name, :color, :description] if attrs.empty? render_api_error!('Required parameters "new_name" or "color" ' \ diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 93052fba06..4e7de8867b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -56,7 +56,7 @@ module API end merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort) - present paginate(merge_requests), with: Entities::MergeRequest + present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user end # Create MR @@ -94,7 +94,7 @@ module API merge_request.add_labels_by_names(params[:labels].split(",")) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors end @@ -130,7 +130,7 @@ module API authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user end # Show MR commits @@ -162,7 +162,7 @@ module API merge_request = user_project.merge_requests. find(params[:merge_request_id]) authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequestChanges + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end # Update MR @@ -204,7 +204,7 @@ module API merge_request.add_labels_by_names(params[:labels].split(",")) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors end @@ -246,7 +246,7 @@ module API execute(merge_request) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user end # Cancel Merge if Merge When build succeeds is enabled @@ -325,7 +325,7 @@ module API get "#{path}/closes_issues" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user end end end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index c5cd73943f..84b4d4cdd6 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -3,17 +3,35 @@ module API class Milestones < Grape::API before { authenticate! } + helpers do + def filter_milestones_state(milestones, state) + case state + when 'active' then milestones.active + when 'closed' then milestones.closed + else milestones + end + end + end + resource :projects do # Get a list of project milestones # # Parameters: - # id (required) - The ID of a project + # id (required) - The ID of a project + # state (optional) - Return "active" or "closed" milestones # Example Request: # GET /projects/:id/milestones + # GET /projects/:id/milestones?iid=42 + # GET /projects/:id/milestones?state=active + # GET /projects/:id/milestones?state=closed get ":id/milestones" do authorize! :read_milestone, user_project - present paginate(user_project.milestones), with: Entities::Milestone + milestones = user_project.milestones + milestones = filter_milestones_state(milestones, params[:state]) + milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? + + present paginate(milestones), with: Entities::Milestone end # Get a single project milestone @@ -87,7 +105,7 @@ module API authorize! :read_milestone, user_project @milestone = user_project.milestones.find(params[:milestone_id]) - present paginate(@milestone.issues), with: Entities::Issue + present paginate(@milestone.issues), with: Entities::Issue, current_user: current_user end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 174473f537..a1c98f5e8f 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -112,6 +112,23 @@ module API end end + # Delete a +noteable+ note + # + # Parameters: + # id (required) - The ID of a project + # noteable_id (required) - The ID of an issue, MR, or snippet + # node_id (required) - The ID of a note + # Example Request: + # DELETE /projects/:id/issues/:noteable_id/notes/:note_id + # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id + delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + authorize! :admin_note, note + + ::Notes::DeleteService.new(user_project, current_user).execute(note) + + present note, with: Entities::Note + end end end end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index c756bb479f..4aefdf319c 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -93,12 +93,17 @@ module API # Example Request: # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do - authorize! :admin_project, user_project project_member = user_project.project_members.find_by(user_id: params[:user_id]) - unless project_member.nil? - project_member.destroy - else + + unless current_user.can?(:admin_project, user_project) || + current_user.can?(:destroy_project_member, project_member) + forbidden! + end + + if project_member.nil? { message: "Access revoked", id: params[:user_id].to_i } + else + project_member.destroy end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6fcb5261e4..24b3100547 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -244,6 +244,34 @@ module API end end + # Archive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/archive + post ':id/archive' do + authorize!(:archive_project, user_project) + + user_project.archive! + + present user_project, with: Entities::Project + end + + # Unarchive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/unarchive + post ':id/unarchive' do + authorize!(:archive_project, user_project) + + user_project.unarchive! + + present user_project, with: Entities::Project + end + # Remove project # # Parameters: diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 2d8a9e51bb..d1a10479e4 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -16,6 +16,20 @@ module API with: Entities::RepoTag, project: user_project end + # Get a single repository tag + # + # Parameters: + # id (required) - The ID of a project + # tag_name (required) - The name of the tag + # Example Request: + # GET /projects/:id/repository/tags/:tag_name + get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do + tag = user_project.repository.find_tag(params[:tag_name]) + not_found!('Tag') unless tag + + present tag, with: Entities::RepoTag, project: user_project + end + # Create tag # # Parameters: diff --git a/lib/api/users.rb b/lib/api/users.rb index 13ab17c690..0a14bac07c 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -58,6 +58,7 @@ module API # extern_uid - External authentication provider UID # provider - External provider # bio - Bio + # location - Location of the user # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false # confirm - Require user confirmation - true (default) or false @@ -67,7 +68,7 @@ module API post do authenticated_as_admin! required_attributes! [:email, :password, :name, :username] - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external] admin = attrs.delete(:admin) confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) user = User.build_user(attrs) @@ -106,6 +107,7 @@ module API # website_url - Website url # projects_limit - Limit projects each user can create # bio - Bio + # location - Location of the user # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false # external - Flags the user as external - true or false(default) @@ -114,7 +116,7 @@ module API put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 783fcfb61a..4fc3443ac6 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -48,4 +48,23 @@ class AwardEmoji JSON.parse(File.read(json_path)) end end + + # Returns an Array of Emoji names and their asset URLs. + def self.urls + @urls ||= begin + path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + prefix = Gitlab::Application.config.assets.prefix + digest = Gitlab::Application.config.assets.digest + + JSON.parse(File.read(path)).map do |hash| + if digest + fname = "#{hash['unicode']}-#{hash['digest']}" + else + fname = hash['unicode'] + end + + { name: hash['name'], path: "#{prefix}/#{fname}.png" } + end + end + end end diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb index 905c4c0144..3eb544dfef 100644 --- a/lib/banzai/filter.rb +++ b/lib/banzai/filter.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/string/output_safety' - module Banzai module Filter def self.[](name) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 34c3891347..b8962379cb 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -11,15 +11,19 @@ module Banzai end def self.object_name - object_class.name.underscore + @object_name ||= object_class.name.underscore end def self.object_sym - object_name.to_sym + @object_sym ||= object_name.to_sym end def self.data_reference - "data-#{object_name.dasherize}" + @data_reference ||= "data-#{object_name.dasherize}" + end + + def self.object_class_title + @object_title ||= object_class.name.titleize end # Public: Find references in text (like `!123` for merge requests) @@ -53,6 +57,10 @@ module Banzai self.class.object_sym end + def object_class_title + self.class.object_class_title + end + def references_in(*args, &block) self.class.references_in(*args, &block) end @@ -62,36 +70,81 @@ module Banzai # Example: project.merge_requests.find end + def find_object_cached(project, id) + if RequestStore.active? + cache = find_objects_cache[object_class][project.id] + + get_or_set_cache(cache, id) { find_object(project, id) } + else + find_object(project, id) + end + end + + def project_from_ref_cache(ref) + if RequestStore.active? + cache = project_refs_cache + + get_or_set_cache(cache, ref) { project_from_ref(ref) } + else + project_from_ref(ref) + end + end + def url_for_object(object, project) # Implement in child class # Example: project_merge_request_url end - def call - if object_class.reference_pattern - # `#123` - replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content, object_class.reference_pattern) - end + def url_for_object_cached(object, project) + if RequestStore.active? + cache = url_for_object_cache[object_class][project.id] - # `[Issue](#123)`, which is turned into - # `Issue` - replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| - object_link_filter(link, object_class.reference_pattern, link_text: text) - end + get_or_set_cache(cache, object) { url_for_object(object, project) } + else + url_for_object(object, project) end + end - if object_class.link_reference_pattern - # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into - # `http://gitlab.example.com/namespace/project/issues/123` - replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| - object_link_filter(text, object_class.link_reference_pattern) - end + def call + return doc if project.nil? - # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into - # `Issue` - replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| - object_link_filter(link, object_class.link_reference_pattern, link_text: text) + ref_pattern = object_class.reference_pattern + link_pattern = object_class.link_reference_pattern + + each_node do |node| + if text_node?(node) && ref_pattern + replace_text_when_pattern_matches(node, ref_pattern) do |content| + object_link_filter(content, ref_pattern) + end + + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if ref_pattern && link =~ /\A#{ref_pattern}\z/ + replace_link_node_with_href(node, link) do + object_link_filter(link, ref_pattern, link_text: text) + end + + next + end + + next unless link_pattern + + if link == text && text =~ /\A#{link_pattern}/ + replace_link_node_with_text(node, link) do + object_link_filter(text, link_pattern) + end + + next + end + + if link =~ /\A#{link_pattern}\z/ + replace_link_node_with_href(node, link) do + object_link_filter(link, link_pattern, link_text: text) + end + + next + end + end end end @@ -109,9 +162,9 @@ module Banzai # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_text: nil) references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref(project_ref) + project = project_from_ref_cache(project_ref) - if project && object = find_object(project, id) + if project && object = find_object_cached(project, id) title = object_link_title(object) klass = reference_class(object_sym) @@ -121,8 +174,11 @@ module Banzai object_sym => object.id ) - url = matches[:url] if matches.names.include?("url") - url ||= url_for_object(object, project) + if matches.names.include?("url") && matches[:url] + url = matches[:url] + else + url = url_for_object_cached(object, project) + end text = link_text || object_link_text(object, matches) @@ -146,7 +202,7 @@ module Banzai end def object_link_title(object) - "#{object_class.name.titleize}: #{object.title}" + "#{object_class_title}: #{object.title}" end def object_link_text(object, matches) @@ -157,6 +213,32 @@ module Banzai text end + + private + + def project_refs_cache + RequestStore[:banzai_project_refs] ||= {} + end + + def find_objects_cache + RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + + def url_for_object_cache + RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + + def get_or_set_cache(cache, key) + if cache.key?(key) + cache[key] + else + cache[key] = yield + end + end end end end diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 856f56fb17..fac7dad324 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -1,4 +1,3 @@ -require 'html/pipeline/filter' require 'uri' module Banzai diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 470727ee31..b469ea0f62 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -43,7 +43,7 @@ module Banzai end def url_for_object(range, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 713a56ba94..bd88207326 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -37,7 +37,7 @@ module Banzai end def url_for_object(commit, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index 207437ba7c..d25de90067 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -1,7 +1,3 @@ -require 'action_controller' -require 'gitlab_emoji' -require 'html/pipeline/filter' - module Banzai module Filter # HTML filter that replaces :emoji: with images. diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index edc2638690..37344b9057 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -35,15 +35,29 @@ module Banzai def call # Early return if the project isn't using an external tracker - return doc if project.nil? || project.default_issues_tracker? + return doc if project.nil? || default_issues_tracker? - replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| - issue_link_filter(content) + ref_pattern = ExternalIssue.reference_pattern + ref_start_pattern = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + issue_link_filter(content) + end + + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_start_pattern + replace_link_node_with_href(node, link) do + issue_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text| - issue_link_filter(link, link_text: text) - end + doc end # Replace `JIRA-123` issue references in text with links to the referenced @@ -76,6 +90,21 @@ module Banzai def url_for_issue(*args) IssuesHelper.url_for_issue(*args) end + + def default_issues_tracker? + if RequestStore.active? + default_issues_tracker_cache[project.id] ||= + project.default_issues_tracker? + else + project.default_issues_tracker? + end + end + + private + + def default_issues_tracker_cache + RequestStore[:banzai_default_issues_tracker_cache] ||= {} + end end end end diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 8d368f3b9e..d179bea181 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -1,5 +1,3 @@ -require 'html/pipeline/filter' - module Banzai module Filter # HTML Filter to add a `rel="nofollow"` attribute to external links diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index f31f921903..d08267a9d6 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -1,6 +1,3 @@ -require 'banzai' -require 'html/pipeline/filter' - module Banzai module Filter # HTML Filter for parsing Gollum's tags in HTML. It's only parses the @@ -121,7 +118,7 @@ module Banzai end if path - content_tag(:img, nil, src: path) + content_tag(:img, nil, src: path, class: 'gfm') end end @@ -147,12 +144,18 @@ module Banzai # if it is not. def process_page_link_tag(parts) if parts.size == 1 - url = parts[0].strip + reference = parts[0].strip else - name, url = *parts.compact.map(&:strip) + name, reference = *parts.compact.map(&:strip) end - content_tag(:a, name || url, href: url) + if url?(reference) + href = reference + else + href = ::File.join(project_wiki_base_path, reference) + end + + content_tag(:a, name || reference, href: href, class: 'gfm') end def project_wiki diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb new file mode 100644 index 0000000000..ccd106860b --- /dev/null +++ b/lib/banzai/filter/image_link_filter.rb @@ -0,0 +1,27 @@ +module Banzai + module Filter + # HTML filter that wraps links around inline images. + class ImageLinkFilter < HTML::Pipeline::Filter + + # Find every image that isn't already wrapped in an `a` tag, create + # a new node (a link to the image source), copy the image as a child + # of the anchor, and then replace the img with the link-wrapped version. + def call + doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| + + link = doc.document.create_element( + 'a', + class: 'no-attachment-icon', + href: img['src'], + target: '_blank' + ) + + link.children = img.clone + img.replace(link) + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8147e5ed3c..a2987850d0 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -31,7 +31,7 @@ module Banzai end def url_for_object(label, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index 0659fed141..9b209533a8 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -1,5 +1,3 @@ -require 'html/pipeline/filter' - module Banzai module Filter class MarkdownFilter < HTML::Pipeline::TextFilter diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index 57c7170899..cad38a5185 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(mr, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index e88b27c1fa..4cb8217802 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Filter # HTML filter that replaces milestone references with links. @@ -13,7 +11,7 @@ module Banzai end def url_for_object(issue, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index 7141ed7c9b..e589b5df6e 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -1,5 +1,3 @@ -require 'html/pipeline/filter' - module Banzai module Filter # HTML filter that removes references to records that the current user does diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 132f0a4bd9..31386cf851 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -1,6 +1,3 @@ -require 'active_support/core_ext/string/output_safety' -require 'html/pipeline/filter' - module Banzai module Filter # Base class for GitLab Flavored Markdown reference filters. @@ -55,18 +52,13 @@ module Banzai html.html_safe? ? html : ERB::Util.html_escape_once(html) end - def ignore_parents - @ignore_parents ||= begin - # Don't look for references in text nodes that are children of these - # elements. + def ignore_ancestor_query + @ignore_ancestor_query ||= begin parents = %w(pre code a style) parents << 'blockquote' if context[:ignore_blockquotes] - parents.to_set - end - end - def ignored_ancestry?(node) - has_ancestor?(node, ignore_parents) + parents.map { |n| "ancestor::#{n}" }.join(' or ') + end end def project @@ -77,120 +69,67 @@ module Banzai "gfm gfm-#{type}" end - # Iterate through the document's text nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` AND - # * The node is not an ancestor of an ignored node type - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node's existing content and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_text_nodes_matching(pattern) - return doc if project.nil? - - search_text_nodes(doc).each do |node| - next if ignored_ancestry?(node) - next unless node.text =~ pattern - - content = node.to_html - - html = yield content - - next if html == content - - node.replace(html) - end - - doc - end - - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_text(pattern) - return doc if project.nil? - - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') - - link = node.attr('href') - text = node.text - - next unless link && text - - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - # Ignore ending punctionation like periods or commas - next unless link == text && text =~ /\A#{pattern}/ - - html = yield text - - next if html == text - - node.replace(html) - end - - doc - end - - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's HREF matches `pattern` - # - # pattern - Regex pattern against which to match the node's HREF - # - # Yields the current node's String HREF and String content. - # The result of the block will replace the node and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_href(pattern) - return doc if project.nil? - - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') - - link = node.attr('href') - text = node.text - - next unless link && text - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - next unless link && link =~ /\A#{pattern}\z/ - - html = yield link, text - - next if html == link - - node.replace(html) - end - - doc - end - # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! def validate needs :project end + + # Iterates over all and text() nodes in a document. + # + # Nodes are skipped whenever their ancestor is one of the nodes returned + # by `ignore_ancestor_query`. Link tags are not processed if they have a + # "gfm" class or the "href" attribute is empty. + def each_node + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] + | descendant-or-self::a[ + not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") + ]} + + doc.xpath(query).each do |node| + yield node + end + end + + # Yields the link's URL and text whenever the node is a valid tag. + def yield_valid_link(node) + link = CGI.unescape(node.attr('href').to_s) + text = node.text + + return unless link.force_encoding('UTF-8').valid_encoding? + + yield link, text + end + + def replace_text_when_pattern_matches(node, pattern) + return unless node.text =~ pattern + + content = node.to_html + html = yield content + + node.replace(html) unless content == html + end + + def replace_link_node_with_text(node, link) + html = yield + + node.replace(html) unless html == node.text + end + + def replace_link_node_with_href(node, link) + html = yield + + node.replace(html) unless html == link + end + + def text_node?(node) + node.is_a?(Nokogiri::XML::Text) + end + + def element_node?(node) + node.is_a?(Nokogiri::XML::Element) + end end end end diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb index 86d484feb9..96fdb06304 100644 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ b/lib/banzai/filter/reference_gatherer_filter.rb @@ -1,5 +1,3 @@ -require 'html/pipeline/filter' - module Banzai module Filter # HTML filter that gathers all referenced records that the current user has diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 41380627d3..ea21c7b041 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -1,4 +1,3 @@ -require 'html/pipeline/filter' require 'uri' module Banzai diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index e801151960..42dbab9d27 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -1,6 +1,3 @@ -require 'html/pipeline/filter' -require 'html/pipeline/sanitization_filter' - module Banzai module Filter # Sanitize HTML diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index c870a42f74..d507eb5ebe 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(snippet, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 8c5855e5ff..62a79c62e2 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -1,4 +1,3 @@ -require 'html/pipeline/filter' require 'rouge/plugins/redcarpet' module Banzai diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 4056dcd6d6..a4eda6fdf7 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -1,5 +1,3 @@ -require 'html/pipeline/filter' - module Banzai module Filter # HTML filter that adds an anchor child element to all Headers in a diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index f642aee096..7edfe5ade2 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -1,4 +1,3 @@ -require 'html/pipeline/filter' require 'uri' module Banzai diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 24f16f8b54..eea3af842b 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -59,13 +59,28 @@ module Banzai end def call - replace_text_nodes_matching(User.reference_pattern) do |content| - user_link_filter(content) + return doc if project.nil? + + ref_pattern = User.reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + user_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + user_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(User.reference_pattern) do |link, text| - user_link_filter(link, link_text: text) - end + doc end # Replace `@user` user references in text with links to the referenced @@ -90,7 +105,7 @@ module Banzai private def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def link_class diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb new file mode 100644 index 0000000000..06d10c9850 --- /dev/null +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -0,0 +1,56 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that "fixes" relative links to files in a repository. + # + # Context options: + # :project_wiki + class WikiLinkFilter < HTML::Pipeline::Filter + + def call + return doc unless project_wiki? + + doc.search('a:not(.gfm)').each do |el| + process_link_attr el.attribute('href') + end + + doc + end + + protected + + def project_wiki? + !context[:project_wiki].nil? + end + + def process_link_attr(html_attr) + return if html_attr.blank? || file_reference?(html_attr) + + uri = URI(html_attr.value) + if uri.relative? && uri.path.present? + html_attr.value = rebuild_wiki_uri(uri).to_s + end + rescue URI::Error + # noop + end + + def rebuild_wiki_uri(uri) + uri.path = ::File.join(project_wiki_base_path, uri.path) + uri + end + + def file_reference?(html_attr) + !File.extname(html_attr.value).blank? + end + + def project_wiki + context[:project_wiki] + end + + def project_wiki_base_path + project_wiki && project_wiki.wiki_base_path + end + end + end +end diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb index e4e2f3f228..58e3e81209 100644 --- a/lib/banzai/filter/yaml_front_matter_filter.rb +++ b/lib/banzai/filter/yaml_front_matter_filter.rb @@ -1,6 +1,3 @@ -require 'html/pipeline/filter' -require 'yaml' - module Banzai module Filter class YamlFrontMatterFilter < HTML::Pipeline::Filter diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb index f60966c3c0..321fd5bbe1 100644 --- a/lib/banzai/pipeline/base_pipeline.rb +++ b/lib/banzai/pipeline/base_pipeline.rb @@ -1,5 +1,3 @@ -require 'html/pipeline' - module Banzai module Pipeline class BasePipeline diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8cd4b50e65..ed3cfd6b02 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -7,6 +7,7 @@ module Banzai Filter::SanitizationFilter, Filter::UploadLinkFilter, + Filter::ImageLinkFilter, Filter::EmojiFilter, Filter::TableOfContentsFilter, Filter::AutolinkFilter, diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index 9b4ff0f0f8..c37b8e71cb 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -1,11 +1,11 @@ -require 'banzai' - module Banzai module Pipeline class WikiPipeline < FullPipeline def self.filters - @filters ||= super.insert_after(Filter::TableOfContentsFilter, - Filter::GollumTagsFilter) + @filters ||= begin + super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) + .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) + end end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ae714c87dc..c14a9c4c72 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -19,8 +19,10 @@ module Banzai cache_key = full_cache_key(cache_key, context[:pipeline]) if cache_key - Rails.cache.fetch(cache_key) do - cacheless_render(text, context) + Gitlab::Metrics.measure(:banzai_cached_render) do + Rails.cache.fetch(cache_key) do + cacheless_render(text, context) + end end else cacheless_render(text, context) @@ -64,13 +66,15 @@ module Banzai private def self.cacheless_render(text, context = {}) - result = render_result(text, context) + Gitlab::Metrics.measure(:banzai_cacheless_render) do + result = render_result(text, context) - output = result[:output] - if output.respond_to?(:to_html) - output.to_html - else - output.to_s + output = result[:output] + if output.respond_to?(:to_html) + output.to_html + else + output.to_s + end end end diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb new file mode 100644 index 0000000000..e5e9fab3f5 --- /dev/null +++ b/lib/gitlab/badge/build.rb @@ -0,0 +1,46 @@ +module Gitlab + module Badge + ## + # Build badge + # + class Build + include Gitlab::Application.routes.url_helpers + include ActionView::Helpers::AssetTagHelper + include ActionView::Helpers::UrlHelper + + def initialize(project, ref) + @project, @ref = project, ref + @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) + end + + def type + 'image/svg+xml' + end + + def data + File.read(@image[:path]) + end + + def to_s + @image[:name].sub(/\.svg$/, '') + end + + def to_html + link_to(image_tag(image_url, alt: 'build status'), link_url) + end + + def to_markdown + "[![build status](#{image_url})](#{link_url})" + end + + def image_url + build_namespace_project_badges_url(@project.namespace, + @project, @ref, format: :svg) + end + + def link_url + namespace_project_commits_url(@project.namespace, @project, id: @ref) + end + end + end +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 761b63e98f..1acc22fe5b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -21,7 +21,6 @@ module Gitlab default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 41f0edcaf7..8f9be6cd9a 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,7 +5,7 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 2ca21af5bc..97ef9851d7 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -45,12 +45,12 @@ module Gitlab note = create_note(reply) unless note.persisted? - message = "The comment could not be created for the following reasons:" + msg = "The comment could not be created for the following reasons:" note.errors.full_messages.each do |error| - message << "\n\n- #{error}" + msg << "\n\n- #{error}" end - raise InvalidNoteError, message + raise InvalidNoteError, msg end end @@ -63,9 +63,24 @@ module Gitlab end def reply_key - reply_key = nil + key_from_to_header || key_from_additional_headers + end + + def key_from_to_header + key = nil message.to.each do |address| - reply_key = Gitlab::IncomingEmail.key_from_address(address) + key = Gitlab::IncomingEmail.key_from_address(address) + break if key + end + + key + end + + def key_from_additional_headers + reply_key = nil + + Array(message.references).each do |message_id| + reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id) break if reply_key end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 2ef50286b1..c2260a5f7a 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -15,6 +15,25 @@ module Gitlab # seconds then two overlapping operations may hold a lease for the same # key at the same time. # + # This class has no 'cancel' method. I originally decided against adding + # it because it would add complexity and a false sense of security. The + # complexity: instead of setting '1' we would have to set a UUID, and to + # delete it we would have to execute Lua on the Redis server to only + # delete the key if the value was our own UUID. Otherwise there is a + # chance that when you intend to cancel your lease you actually delete + # someone else's. The false sense of security: you cannot design your + # system to rely too much on the lease being cancelled after use because + # the calling (Ruby) process may crash or be killed. You _cannot_ count + # on begin/ensure blocks to cancel a lease, because the 'ensure' does + # not always run. Think of 'kill -9' from the Unicorn master for + # instance. + # + # If you find that leases are getting in your way, ask yourself: would + # it be enough to lower the lease timeout? Another thing that might be + # appropriate is to only use a lease for bulk/automated operations, and + # to ignore the lease when you get a single 'manual' user request (a + # button click). + # class ExclusiveLease def initialize(key, timeout:) @key, @timeout = key, timeout @@ -24,9 +43,13 @@ module Gitlab # false if the lease is already taken. def try_obtain # Performing a single SET is atomic - !!redis.set(redis_key, '1', nx: true, ex: @timeout) + Gitlab::Redis.with do |redis| + !!redis.set(redis_key, '1', nx: true, ex: @timeout) + end end + # No #cancel method. See comments above! + private def redis diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb index 431d50882f..2152182b37 100644 --- a/lib/gitlab/fogbugz_import/client.rb +++ b/lib/gitlab/fogbugz_import/client.rb @@ -26,7 +26,7 @@ module Gitlab def user_map users = {} res = @api.command(:listPeople) - res['people']['person'].each do |user| + [res['people']['person']].flatten.each do |user| users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] } end users diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index a1c6ee7bd6..78d7a4f27c 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -34,16 +34,21 @@ module Gitlab @source_project = source_project @current_user = current_user @original_html = markdown(text) + @pattern = Gitlab::ReferenceExtractor.references_pattern end def rewrite(target_project) - pattern = Gitlab::ReferenceExtractor.references_pattern + return @text unless needs_rewrite? - @text.gsub(pattern) do |reference| + @text.gsub(@pattern) do |reference| unfold_reference(reference, Regexp.last_match, target_project) end end + def needs_rewrite? + @text =~ @pattern + end + private def unfold_reference(reference, match, target_project) diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb new file mode 100644 index 0000000000..abc8c8c55e --- /dev/null +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -0,0 +1,51 @@ +module Gitlab + module Gfm + ## + # Class that rewrites markdown links for uploads + # + # Using a pattern defined in `FileUploader` it copies files to a new + # project and rewrites all links to uploads in in a given text. + # + # + class UploadsRewriter + def initialize(text, source_project, _current_user) + @text = text + @source_project = source_project + @pattern = FileUploader::MARKDOWN_PATTERN + end + + def rewrite(target_project) + return @text unless needs_rewrite? + + @text.gsub(@pattern) do |markdown| + file = find_file(@source_project, $~[:secret], $~[:file]) + return markdown unless file.try(:exists?) + + new_uploader = FileUploader.new(target_project) + new_uploader.store!(file) + new_uploader.to_markdown + end + end + + def needs_rewrite? + files.any? + end + + def files + referenced_files = @text.scan(@pattern).map do + find_file(@source_project, $~[:secret], $~[:file]) + end + + referenced_files.compact.select(&:exists?) + end + + private + + def find_file(project, secret, file) + uploader = FileUploader.new(project, secret) + uploader.retrieve_from_store!(file) + uploader.file + end + end + end +end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 9068d79c95..8ce9d32abe 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,13 +1,10 @@ module Gitlab module IncomingEmail class << self - def enabled? - config.enabled && address_formatted_correctly? - end + FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze - def address_formatted_correctly? - config.address && - config.address.include?("%{key}") + def enabled? + config.enabled && config.address end def reply_address(key) @@ -24,6 +21,13 @@ module Gitlab match[1] end + def key_from_fallback_reply_message_id(message_id) + match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX) + return unless match + + match[1] + end + def config Gitlab.config.incoming_email end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index da4435c730..f2b649e50a 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -33,7 +33,10 @@ module Gitlab def allowed? if ldap_user - return true unless ldap_config.active_directory + unless ldap_config.active_directory + user.activate if user.ldap_blocked? + return true + end # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 88a265c6af..2a0a5629be 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -70,6 +70,40 @@ module Gitlab value.to_s.gsub('=', '\\=') end + # Measures the execution time of a block. + # + # Example: + # + # Gitlab::Metrics.measure(:find_by_username_duration) do + # User.find_by_username(some_username) + # end + # + # name - The name of the field to store the execution time in. + # + # Returns the value yielded by the supplied block. + def self.measure(name) + trans = current_transaction + + return yield unless trans + + real_start = Time.now.to_f + cpu_start = System.cpu_time + + retval = yield + + cpu_stop = System.cpu_time + real_stop = Time.now.to_f + + real_time = (real_stop - real_start) * 1000.0 + cpu_time = cpu_stop - cpu_start + + trans.increment("#{name}_real_time", real_time) + trans.increment("#{name}_cpu_time", cpu_time) + trans.increment("#{name}_call_count", 1) + + retval + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? @@ -81,5 +115,11 @@ module Gitlab new(udp: { host: host, port: port }) end end + + private + + def self.current_transaction + Transaction.current + end end end diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb index 7ea9555cc8..1cd1ca30f7 100644 --- a/lib/gitlab/metrics/metric.rb +++ b/lib/gitlab/metrics/metric.rb @@ -2,6 +2,8 @@ module Gitlab module Metrics # Class for storing details of a single metric (label, value, etc). class Metric + JITTER_RANGE = 0.000001..0.001 + attr_reader :series, :values, :tags, :created_at # series - The name of the series (as a String) to store the metric in. @@ -16,11 +18,29 @@ module Gitlab # Returns a Hash in a format that can be directly written to InfluxDB. def to_hash + # InfluxDB overwrites an existing point if a new point has the same + # series, tag set, and timestamp. In a highly concurrent environment + # this means that using the number of seconds since the Unix epoch is + # inevitably going to collide with another timestamp. For example, two + # Rails requests processed by different processes may end up generating + # metrics using the _exact_ same timestamp (in seconds). + # + # Due to the way InfluxDB is set up there's no solution to this problem, + # all we can do is lower the amount of collisions. We do this by using + # Time#to_f which returns the seconds as a Float providing greater + # accuracy. We then add a small random value that is large enough to + # distinguish most timestamps but small enough to not alter the amount + # of seconds. + # + # See https://gitlab.com/gitlab-com/operations/issues/175 for more + # information. + time = @created_at.to_f + rand(JITTER_RANGE) + { series: @series, tags: @tags, values: @values, - timestamp: @created_at.to_i * 1_000_000_000 + timestamp: (time * 1_000_000_000).to_i } end end diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb new file mode 100644 index 0000000000..49e5f86e6e --- /dev/null +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -0,0 +1,39 @@ +module Gitlab + module Metrics + module Subscribers + # Class for tracking the total time spent in Rails cache calls + class RailsCache < ActiveSupport::Subscriber + attach_to :active_support + + def cache_read(event) + increment(:cache_read_duration, event.duration) + end + + def cache_write(event) + increment(:cache_write_duration, event.duration) + end + + def cache_delete(event) + increment(:cache_delete_duration, event.duration) + end + + def cache_exist?(event) + increment(:cache_exists_duration, event.duration) + end + + def increment(key, duration) + return unless current_transaction + + current_transaction.increment(:cache_duration, duration) + current_transaction.increment(key, duration) + end + + private + + def current_transaction + Transaction.current + end + end + end + end +end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index 8337126527..a7d183b2f9 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -30,6 +30,17 @@ module Gitlab 0 end end + + # THREAD_CPUTIME is not supported on OS X + if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) + def self.cpu_time + Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) + end + else + def self.cpu_time + Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) + end + end end end end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 71cf6a0d88..18523e0aef 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -41,7 +41,7 @@ module Gitlab data[:issue] = note.noteable.hook_attrs elsif note.for_merge_request? data[:merge_request] = note.noteable.hook_attrs - elsif note.for_project_snippet? + elsif note.for_snippet? data[:snippet] = note.noteable.hook_attrs end diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb new file mode 100644 index 0000000000..319447669d --- /dev/null +++ b/lib/gitlab/redis.rb @@ -0,0 +1,48 @@ +module Gitlab + class Redis + CACHE_NAMESPACE = 'cache:gitlab' + + attr_reader :url + + # To be thread-safe we must be careful when writing the class instance + # variables @url and @pool. Because @pool depends on @url we need two + # mutexes to prevent deadlock. + URL_MUTEX = Mutex.new + POOL_MUTEX = Mutex.new + private_constant :URL_MUTEX, :POOL_MUTEX + + def self.url + @url || URL_MUTEX.synchronize { @url = new.url } + end + + def self.with + if @pool.nil? + POOL_MUTEX.synchronize do + @pool = ConnectionPool.new { ::Redis.new(url: url) } + end + end + @pool.with { |redis| yield redis } + end + + def self.redis_store_options + url = new.url + redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) + # Redis::Store does not handle Unix sockets well, so let's do it for them + redis_uri = URI.parse(url) + if redis_uri.scheme == 'unix' + redis_config_hash[:path] = redis_uri.path + end + redis_config_hash + end + + def initialize(rails_env=nil) + rails_env ||= Rails.env + config_file = File.expand_path('../../../config/resque.yml', __FILE__) + + @url = "redis://localhost:6379" + if File.exists?(config_file) + @url =YAML.load_file(config_file)[rails_env] + end + end + end +end diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb deleted file mode 100644 index 4949c6db53..0000000000 --- a/lib/gitlab/redis_config.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Gitlab - class RedisConfig - attr_reader :url - - def self.url - new.url - end - - def self.redis_store_options - url = new.url - redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url) - # Redis::Store does not handle Unix sockets well, so let's do it for them - redis_uri = URI.parse(url) - if redis_uri.scheme == 'unix' - redis_config_hash[:path] = redis_uri.path - end - redis_config_hash - end - - def initialize(rails_env=nil) - rails_env ||= Rails.env - config_file = File.expand_path('../../../config/resque.yml', __FILE__) - - @url = "redis://localhost:6379" - if File.exists?(config_file) - @url =YAML.load_file(config_file)[rails_env] - end - end - end -end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb new file mode 100644 index 0000000000..5132177de5 --- /dev/null +++ b/lib/gitlab/routing.rb @@ -0,0 +1,13 @@ +module Gitlab + module Routing + # Returns the URL helpers Module. + # + # This method caches the output as Rails' "url_helpers" method creates an + # anonymous module every time it's called. + # + # Returns a Module. + def self.url_helpers + @url_helpers ||= Gitlab::Application.routes.url_helpers + end + end +end diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb new file mode 100644 index 0000000000..32c1c9ec5b --- /dev/null +++ b/lib/gitlab/saml/auth_hash.rb @@ -0,0 +1,19 @@ +module Gitlab + module Saml + class AuthHash < Gitlab::OAuth::AuthHash + + def groups + get_raw(Gitlab::Saml::Config.groups) + end + + private + + def get_raw(key) + # Needs to call `all` because of https://git.io/vVo4u + # otherwise just the first value is returned + auth_hash.extra[:raw_info].all[key] + end + + end + end +end diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb new file mode 100644 index 0000000000..0f40c00f54 --- /dev/null +++ b/lib/gitlab/saml/config.rb @@ -0,0 +1,21 @@ +module Gitlab + module Saml + class Config + + class << self + def options + Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' } + end + + def groups + options[:groups_attribute] + end + + def external_groups + options[:external_groups] + end + end + + end + end +end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index b1e30110ef..dba4bbfc89 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -18,7 +18,7 @@ module Gitlab @user ||= find_or_create_ldap_user end - if auto_link_saml_enabled? + if auto_link_saml_user? @user ||= find_by_email end @@ -26,6 +26,16 @@ module Gitlab @user ||= build_new_user end + if external_users_enabled? && @user + # Check if there is overlap between the user's groups and the external groups + # setting then set user as external or internal. + if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? + @user.external = false + else + @user.external = true + end + end + @user end @@ -37,11 +47,24 @@ module Gitlab end end + def changed? + return true unless gl_user + gl_user.changed? || gl_user.identities.any?(&:changed?) + end + protected - def auto_link_saml_enabled? + def auto_link_saml_user? Gitlab.config.omniauth.auto_link_saml_user end + + def external_users_enabled? + !Gitlab::Saml::Config.external_groups.nil? + end + + def auth_hash=(auth_hash) + @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash) + end end end end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 6f0d02cafd..f301d42939 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,7 +1,8 @@ module Gitlab class UrlBuilder - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers include GitlabRoutingHelper + include ActionView::RecordIdentifier def initialize(type) @type = type @@ -37,19 +38,16 @@ module Gitlab namespace_project_commit_url(namespace_id: note.project.namespace, id: note.commit_id, project_id: note.project, - anchor: "note_#{note.id}") + anchor: dom_id(note)) elsif note.for_issue? issue = Issue.find(note.noteable_id) - issue_url(issue, - anchor: "note_#{note.id}") + issue_url(issue, anchor: dom_id(note)) elsif note.for_merge_request? merge_request = MergeRequest.find(note.noteable_id) - merge_request_url(merge_request, - anchor: "note_#{note.id}") - elsif note.for_project_snippet? + merge_request_url(merge_request, anchor: dom_id(note)) + elsif note.for_snippet? snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, - anchor: "note_#{note.id}") + project_snippet_url(snippet, anchor: dom_id(note)) end end end diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 51e746ef92..2214f85520 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -4,18 +4,19 @@ namespace :cache do desc "GitLab | Clear redis cache" task :clear => :environment do - redis = Redis.new(url: Gitlab::RedisConfig.url) - cursor = REDIS_SCAN_START_STOP - loop do - cursor, keys = redis.scan( - cursor, - match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", - count: CLEAR_BATCH_SIZE - ) - - redis.del(*keys) if keys.any? - - break if cursor == REDIS_SCAN_START_STOP + Gitlab::Redis.with do |redis| + cursor = REDIS_SCAN_START_STOP + loop do + cursor, keys = redis.scan( + cursor, + match: "#{Gitlab::Redis::CACHE_NAMESPACE}*", + count: CLEAR_BATCH_SIZE + ) + + redis.del(*keys) if keys.any? + + break if cursor == REDIS_SCAN_START_STOP + end end end end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index cfaf4a129b..7ec00a898f 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -1,19 +1,39 @@ -# This task will generate a standard and Retina sprite of all of the current -# Gemojione Emojis, with the accompanying SCSS map. -# -# It will not appear in `rake -T` output, and the dependent gems are not -# included in the Gemfile by default, because this task will only be needed -# occasionally, such as when new Emojis are added to Gemojione. - -begin - require 'sprite_factory' - require 'rmagick' -rescue LoadError - # noop -end - namespace :gemojione do + desc 'Generates Emoji SHA256 digests' + task digests: :environment do + require 'digest/sha2' + require 'json' + + dir = Gemojione.index.images_path + + digests = AwardEmoji.emojis.map do |name, emoji_hash| + fpath = File.join(dir, "#{emoji_hash['unicode']}.png") + digest = Digest::SHA256.file(fpath).hexdigest + + { name: name, unicode: emoji_hash['unicode'], digest: digest } + end + + out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + + File.open(out, 'w') do |handle| + handle.write(JSON.pretty_generate(digests)) + end + end + + # This task will generate a standard and Retina sprite of all of the current + # Gemojione Emojis, with the accompanying SCSS map. + # + # It will not appear in `rake -T` output, and the dependent gems are not + # included in the Gemfile by default, because this task will only be needed + # occasionally, such as when new Emojis are added to Gemojione. task sprite: :environment do + begin + require 'sprite_factory' + require 'rmagick' + rescue LoadError + # noop + end + check_requirements! SIZE = 20 diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 27ed57efe5..effb8eb600 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -623,7 +623,6 @@ namespace :gitlab do start_checking "Reply by email" if Gitlab.config.incoming_email.enabled - check_address_formatted_correctly check_imap_authentication if Rails.env.production? @@ -643,20 +642,6 @@ namespace :gitlab do # Checks ######################## - def check_address_formatted_correctly - print "Address formatted correctly? ... " - - if Gitlab::IncomingEmail.address_formatted_correctly? - puts "yes".green - else - puts "no".red - try_fixing_it( - "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder." - ) - fix_and_rerun - end - end - def check_initd_configured_correctly print "Init.d configured correctly? ... " diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb new file mode 100644 index 0000000000..2ba0d48919 --- /dev/null +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Admin::ProjectsController do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + + before do + sign_in(create(:admin)) + end + + describe 'GET /projects' do + render_views + + it 'retrieves the project for the given visibility level' do + get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] + expect(response.body).to match(project.name) + end + + it 'does not retrieve the project' do + get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] + expect(response.body).to_not match(project.name) + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 5b1f65d7af..9ef8ba1b09 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -1,15 +1,14 @@ require 'spec_helper' describe Admin::UsersController do - let(:admin) { create(:admin) } + let(:user) { create(:user) } before do - sign_in(admin) + sign_in(create(:admin)) end describe 'DELETE #user with projects' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:empty_project, namespace: user.namespace) } before do project.team << [user, :developer] @@ -23,8 +22,6 @@ describe Admin::UsersController do end describe 'PUT block/:id' do - let(:user) { create(:user) } - it 'blocks user' do put :block, id: user.username user.reload @@ -50,8 +47,6 @@ describe Admin::UsersController do end context 'manually blocked users' do - let(:user) { create(:user) } - before do user.block end @@ -66,8 +61,6 @@ describe Admin::UsersController do end describe 'PUT unlock/:id' do - let(:user) { create(:user) } - before do request.env["HTTP_REFERER"] = "/" user.lock_access! @@ -95,8 +88,6 @@ describe Admin::UsersController do end describe 'PATCH disable_two_factor' do - let(:user) { create(:user) } - it 'disables 2FA for the user' do expect(user).to receive(:disable_two_factor!) allow(subject).to receive(:user).and_return(user) diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb index db0748f323..5022a3e2c8 100644 --- a/spec/controllers/ci/projects_controller_spec.rb +++ b/spec/controllers/ci/projects_controller_spec.rb @@ -5,6 +5,27 @@ describe Ci::ProjectsController do let!(:project) { create(:project, visibility, ci_id: 1) } let(:ci_id) { project.ci_id } + describe '#index' do + context 'user signed in' do + before do + sign_in(create(:user)) + get(:index) + end + + it 'redirects to /' do + expect(response).to redirect_to(root_path) + end + end + + context 'user not signed in' do + before { get(:index) } + + it 'redirects to sign in page' do + expect(response).to redirect_to(new_user_session_path) + end + end + end + ## # Specs for *deprecated* CI badge # diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index eb0c6ac6d8..b0793cb165 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -23,5 +23,11 @@ describe Groups::MilestonesController do expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) expect(Milestone.where(title: title).count).to eq(2) end + + it "redirects to new when there are no project ids" do + post :create, group_id: group.id, milestone: { title: title, project_ids: [""] } + expect(response).to render_template :new + expect(assigns(:milestone).errors).not_to be_nil + end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 98ae424ed7..8ad7347211 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -93,6 +93,20 @@ describe Projects::BranchesController do end end + describe "POST destroy with HTML format" do + render_views + + it 'returns 303' do + post :destroy, + format: :html, + id: 'foo/bar/baz', + namespace_id: project.namespace.to_param, + project_id: project.to_param + + expect(response.status).to eq(303) + end + end + describe "POST destroy" do render_views diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c5b034dc06..75e6b6f45a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: format) - expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) + expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s) end it "should not escape Html" do diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb new file mode 100644 index 0000000000..d47e4ab9a4 --- /dev/null +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -0,0 +1,49 @@ +require('spec_helper') + +describe Projects::ProjectMembersController do + let(:project) { create(:project) } + let(:another_project) { create(:project, :private) } + let(:user) { create(:user) } + let(:member) { create(:user) } + + before do + project.team << [user, :master] + another_project.team << [member, :guest] + sign_in(user) + end + + describe '#apply_import' do + shared_context 'import applied' do + before do + post(:apply_import, namespace_id: project.namespace.to_param, + project_id: project.to_param, + source_project_id: another_project.id) + end + end + + context 'when user can access source project members' do + before { another_project.team << [user, :guest] } + include_context 'import applied' + + it 'imports source project members' do + expect(project.team_members).to include member + expect(response).to set_flash.to 'Successfully imported' + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + end + end + + context 'when user is not member of a source project' do + include_context 'import applied' + + it 'does not import team members' do + expect(project.team_members).to_not include member + end + + it 'responds with not found' do + expect(response.status).to eq 404 + end + end + end +end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb new file mode 100644 index 0000000000..0f32a30f18 --- /dev/null +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe Projects::SnippetsController do + let(:project) { create(:project_empty_repo, :public, snippets_enabled: true) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + before do + project.team << [user, :master] + project.team << [user2, :master] + end + + describe 'GET #index' do + context 'when the project snippet is private' do + let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } + + context 'when anonymous' do + it 'does not include the private snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).not_to include(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as the author' do + before { sign_in(user) } + + it 'renders the snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).to include(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as a project member' do + before { sign_in(user2) } + + it 'renders the snippet' do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(assigns(:snippets)).to include(project_snippet) + expect(response.status).to eq(200) + end + end + end + end + + %w[show raw].each do |action| + describe "GET ##{action}" do + context 'when the project snippet is private' do + let(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } + + context 'when anonymous' do + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(response.status).to eq(404) + end + end + + context 'when signed in as the author' do + before { sign_in(user) } + + it 'renders the snippet' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(assigns(:snippet)).to eq(project_snippet) + expect(response.status).to eq(200) + end + end + + context 'when signed in as a project member' do + before { sign_in(user2) } + + it 'renders the snippet' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: project_snippet.to_param + + expect(assigns(:snippet)).to eq(project_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when the project snippet does not exist' do + context 'when anonymous' do + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + + expect(response.status).to eq(404) + end + end + + context 'when signed in' do + before { sign_in(user) } + + it 'responds with status 404' do + get action, namespace_id: project.namespace.path, project_id: project.path, id: 42 + + expect(response.status).to eq(404) + end + end + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1893e946f5..069cd917e5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -83,6 +83,28 @@ describe ProjectsController do end end + describe "#update" do + render_views + + let(:admin) { create(:admin) } + + it "sets the repository to the right path after a rename" do + new_path = 'renamed_path' + project_params = { path: new_path } + controller.instance_variable_set(:@project, project) + sign_in(admin) + + put :update, + namespace_id: project.namespace.to_param, + id: project.id, + project: project_params + + expect(project.repository.path).to include(new_path) + expect(assigns(:repository).path).to eq(project.repository.path) + expect(response.status).to eq(200) + end + end + describe "#destroy" do let(:admin) { create(:admin) } diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index 5a104ae7c9..b14d275f7f 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -43,6 +43,28 @@ describe RootController do end end + context 'who has customized their dashboard setting for groups' do + before do + user.update_attribute(:dashboard, 'groups') + end + + it 'redirects to their group list' do + get :index + expect(response).to redirect_to dashboard_groups_path + end + end + + context 'who has customized their dashboard setting for todos' do + before do + user.update_attribute(:dashboard, 'todos') + end + + it 'redirects to their todo list' do + get :index + expect(response).to redirect_to dashboard_todos_path + end + end + context 'who uses the default dashboard setting' do it 'renders the default dashboard' do get :index diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb new file mode 100644 index 0000000000..83cc8ec6d2 --- /dev/null +++ b/spec/controllers/sessions_controller_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe SessionsController do + describe '#create' do + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + + context 'when using standard authentications' do + context 'invalid password' do + it 'does not authenticate user' do + post(:create, user: { login: 'invalid', password: 'invalid' }) + + expect(response) + .to set_flash.now[:alert].to /Invalid login or password/ + end + end + + context 'when using valid password' do + let(:user) { create(:user) } + + it 'authenticates user correctly' do + post(:create, user: { login: user.username, password: user.password }) + + expect(response).to set_flash.to /Signed in successfully/ + expect(subject.current_user). to eq user + end + end + end + + context 'when using two-factor authentication' do + let(:user) { create(:user, :two_factor) } + + def authenticate_2fa(user_params) + post(:create, { user: user_params }, { otp_user_id: user.id }) + end + + ## + # See #14900 issue + # + context 'when authenticating with login and OTP of another user' do + context 'when another user has 2FA enabled' do + let(:another_user) { create(:user, :two_factor) } + + context 'when OTP is valid for another user' do + it 'does not authenticate' do + authenticate_2fa(login: another_user.username, + otp_attempt: another_user.current_otp) + + expect(subject.current_user).to_not eq another_user + end + end + + context 'when OTP is invalid for another user' do + it 'does not authenticate' do + authenticate_2fa(login: another_user.username, + otp_attempt: 'invalid') + + expect(subject.current_user).to_not eq another_user + end + end + + context 'when authenticating with OTP' do + context 'when OTP is valid' do + it 'authenticates correctly' do + authenticate_2fa(otp_attempt: user.current_otp) + + expect(subject.current_user).to eq user + end + end + + context 'when OTP is invalid' do + before { authenticate_2fa(otp_attempt: 'invalid') } + + it 'does not authenticate' do + expect(subject.current_user).to_not eq user + end + + it 'warns about invalid OTP code' do + expect(response).to set_flash.now[:alert] + .to /Invalid two-factor code/ + end + end + end + + context 'when another user does not have 2FA enabled' do + let(:another_user) { create(:user) } + + it 'does not leak that 2FA is disabled for another user' do + authenticate_2fa(login: another_user.username, + otp_attempt: 'invalid') + + expect(response).to set_flash.now[:alert] + .to /Invalid two-factor code/ + end + end + end + end + end + end +end diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploader.rb new file mode 100644 index 0000000000..1b36e21f2b --- /dev/null +++ b/spec/factories/file_uploader.rb @@ -0,0 +1,20 @@ +FactoryGirl.define do + factory :file_uploader do + project + secret nil + + transient do + fixture { 'rails_sample.jpg' } + path { File.join(Rails.root, 'spec/fixtures', fixture) } + file { Rack::Test::UploadedFile.new(path) } + end + + after(:build) do |uploader, evaluator| + uploader.store!(evaluator.file) + end + + initialize_with do + new(project, secret) + end + end +end diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 252bf2747e..19a54946fe 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -13,5 +13,10 @@ FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project association :forked_from_project, factory: :project + + after(:create) do |link| + link.forked_from_project.reload + link.forked_to_project.reload + end end end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 457859deda..62de081661 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' -FactoryGirl.factories.map(&:name).each do |factory_name| - describe "#{factory_name} factory" do - it 'should be valid' do - expect(build(factory_name)).to be_valid +describe 'factories' do + FactoryGirl.factories.each do |factory| + describe "#{factory.name} factory" do + let(:entity) { build(factory.name) } + + it 'does not raise error when created 'do + expect { entity }.to_not raise_error + end + + it 'should be valid', if: factory.build_class < ActiveRecord::Base do + expect(entity).to be_valid + end end end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index dc41be8246..de6aed74fb 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'should have XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: ]*\/>/ + expect(body).to match /Here is the fix: ]*>]*\/><\/a>/ end end end diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb new file mode 100644 index 0000000000..39805da9d0 --- /dev/null +++ b/spec/features/dashboard_issues_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe "Dashboard Issues filtering", feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:milestone) { create(:milestone, project: project) } + + context 'filtering by milestone' do + before do + project.team << [user, :master] + login_as(user) + + create(:issue, project: project, author: user, assignee: user) + create(:issue, project: project, author: user, assignee: user, milestone: milestone) + + visit_issues + end + + it 'should show all issues with no milestone' do + show_milestone_dropdown + + click_link 'No Milestone' + + expect(page).to have_selector('.issue', count: 1) + end + + it 'should show all issues with any milestone' do + show_milestone_dropdown + + click_link 'Any Milestone' + + expect(page).to have_selector('.issue', count: 2) + end + + it 'should show all issues with the selected milestone' do + show_milestone_dropdown + + page.within '.dropdown-content' do + click_link milestone.title + end + + expect(page).to have_selector('.issue', count: 1) + end + end + + def show_milestone_dropdown + click_button 'Milestone' + expect(page).to have_selector('.dropdown-content', visible: true) + end + + def visit_issues + visit issues_dashboard_path + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb new file mode 100644 index 0000000000..41af789aae --- /dev/null +++ b/spec/features/issues/award_emoji_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +describe 'Awards Emoji', feature: true do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'Click award emoji from issue#show' do + let!(:issue) do + create(:issue, + author: @user, + assignee: @user, + project: project) + end + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should increment the thumbsdown emoji', js: true do + find('[data-emoji="thumbsdown"]').click + sleep 2 + expect(thumbsdown_emoji).to have_text("1") + end + + context 'click the thumbsup emoji' do + + it 'should increment the thumbsup emoji', js: true do + find('[data-emoji="thumbsup"]').click + sleep 2 + expect(thumbsup_emoji).to have_text("1") + end + + it 'should decrement the thumbsdown emoji', js: true do + expect(thumbsdown_emoji).to have_text("0") + end + end + + context 'click the thumbsdown emoji' do + + it 'should increment the thumbsdown emoji', js: true do + find('[data-emoji="thumbsdown"]').click + sleep 2 + expect(thumbsdown_emoji).to have_text("1") + end + + it 'should decrement the thumbsup emoji', js: true do + expect(thumbsup_emoji).to have_text("0") + end + end + end + + def thumbsup_emoji + page.all('span.js-counter').first + end + + def thumbsdown_emoji + page.all('span.js-counter').last + end +end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index f6e33f651c..9944518589 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -11,7 +11,41 @@ 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('.issue', count: 1) + end + + context 'filters by upcoming milestone', js: true do + it 'should not show issues with no expiry' do + create(:issue, project: project) + create(:issue, project: project, milestone: milestone) + + visit_issues(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.issue', count: 0) + end + + it 'should show issues in future' do + milestone = create(:milestone, project: project, due_date: Date.tomorrow) + create(:issue, project: project) + create(:issue, project: project, milestone: milestone) + + visit_issues(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.issue', count: 1) + end + + it 'should not show issues in past' do + milestone = create(:milestone, project: project, due_date: Date.yesterday) + create(:issue, project: project) + create(:issue, project: project, milestone: milestone) + + visit_issues(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.issue', count: 0) + end end scenario 'filters by a specific Milestone', js: true do @@ -21,7 +55,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('.issue', count: 1) end def visit_issues(project) @@ -30,8 +64,6 @@ feature 'Issue filtering by Milestone', feature: true do def filter_by_milestone(title) find(".js-milestone-select").click - sleep 0.5 - find(".milestone-filter a", text: title).click - sleep 1 + find(".milestone-filter .dropdown-content a", text: title).click end end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb new file mode 100644 index 0000000000..90822a8c12 --- /dev/null +++ b/spec/features/issues/filter_issues_spec.rb @@ -0,0 +1,119 @@ +require 'rails_helper' + +describe 'Filter issues', feature: true do + + let!(:project) { create(:project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'Filter issues for assignee from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + sleep 2 + end + + context 'assignee', js: true do + it 'should update to current user' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + end + end + + describe 'Filter issues for milestone from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-milestone-select').click + + find('.milestone-filter .dropdown-content a', text: milestone.title).click + + sleep 2 + end + + context 'milestone', js: true do + it 'should update to current milestone' do + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + end + end + + describe 'Filter issues for assignee and label from issues#index' do + + before do + visit namespace_project_issues_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + sleep 2 + + find('.js-label-select').click + + find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + + sleep 2 + end + + context 'assignee and label', js: true do + it 'should update to current assignee and label' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'should not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + + it 'should not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + end +end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb new file mode 100644 index 0000000000..3eb903a93f --- /dev/null +++ b/spec/features/issues/update_issues_spec.rb @@ -0,0 +1,117 @@ +require 'rails_helper' + +feature 'Multiple issue updating from issues#index', feature: true do + let!(:project) { create(:project) } + let!(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + before do + project.team << [user, :master] + login_as(user) + end + + context 'status', js: true do + it 'should be set to closed' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-issue-status').click + + find('.dropdown-menu-status a', text: 'Closed').click + click_update_issues_button + expect(page).to have_selector('.issue', count: 0) + end + + it 'should be set to open' do + create_closed + visit namespace_project_issues_path(project.namespace, project) + + find('.issues-state-filters a', text: 'Closed').click + + find('#check_all_issues').click + find('.js-issue-status').click + + find('.dropdown-menu-status a', text: 'Open').click + click_update_issues_button + expect(page).to have_selector('.issue', count: 0) + end + end + + context 'assignee', js: true do + it 'should update to current user' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-update-assignee').click + + find('.dropdown-menu-user-link', text: user.username).click + click_update_issues_button + + page.within('.issue .controls') do + expect(find('.author_link')["data-original-title"]).to have_content(user.name) + end + end + + it 'should update to unassigned' do + create_assigned + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-update-assignee').click + + click_link 'Unassigned' + click_update_issues_button + + within first('.issue .controls') do + expect(page).to have_no_selector('.author_link') + end + end + end + + context 'milestone', js: true do + let(:milestone) { create(:milestone, project: project) } + + it 'should update milestone' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + + find('.dropdown-menu-milestone a', text: milestone.title).click + click_update_issues_button + + expect(find('.issue')).to have_content milestone.title + end + + it 'should set to no milestone' do + create_with_milestone + visit namespace_project_issues_path(project.namespace, project) + + expect(first('.issue')).to have_content milestone.title + + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + + find('.dropdown-menu-milestone a', text: "No Milestone").click + click_update_issues_button + + expect(first('.issue')).to_not have_content milestone.title + end + end + + def create_closed + create(:issue, project: project, state: :closed) + end + + def create_assigned + create(:issue, project: project, assignee: user) + end + + def create_with_milestone + create(:issue, project: project, milestone: milestone) + end + + def click_update_issues_button + find('.update_selected_issues').click + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index e844e681eb..79000666cc 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -22,7 +22,7 @@ describe 'Issues', feature: true do before do visit edit_namespace_project_issue_path(project.namespace, project, issue) - click_link "Edit" + click_button "Go full screen" end it 'should open new issue popup' do @@ -34,20 +34,7 @@ describe 'Issues', feature: true do fill_in 'issue_title', with: 'bug 345' fill_in 'issue_description', with: 'bug description' end - - it 'does not change issue count' do - expect { click_button 'Save changes' }.to_not change { Issue.count } - end - - it 'should update issue fields' do - click_button 'Save changes' - - expect(page).to have_content @user.name - expect(page).to have_content 'bug 345' - expect(page).to have_content project.name - end end - end describe 'Editing issue assignee' do @@ -70,7 +57,7 @@ describe 'Issues', feature: true do click_button 'Save changes' page.within('.assignee') do - expect(page).to have_content 'None' + expect(page).to have_content 'No assignee - assign yourself' end expect(issue.reload.assignee).to be_nil @@ -198,20 +185,26 @@ describe 'Issues', feature: true do end describe 'update assignee from issue#show' do - let(:issue) { create(:issue, project: project, author: @user) } + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } context 'by autorized user' do - it 'with dropdown menu' do + it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - find('.issuable-sidebar #issue_assignee_id'). - set project.team.members.first.id - click_button 'Update Issue' + page.within('.assignee') do + expect(page).to have_content "#{@user.name}" + end - expect(page).to have_content 'Assignee' - has_select?('issue_assignee_id', - selected: project.team.members.first.name) + find('.block.assignee .edit-link').click + sleep 2 # wait for ajax stuff to complete + first('.dropdown-menu-user-link').click + sleep 2 + page.within('.assignee') do + expect(page).to have_content 'No assignee' + end + + expect(issue.reload.assignee).to be_nil end end @@ -221,8 +214,6 @@ describe 'Issues', feature: true do before :each do project.team << [[guest], :guest] - issue.assignee = @user - issue.save end it 'shows assignee text', js: true do @@ -241,20 +232,23 @@ describe 'Issues', feature: true do context 'by authorized user' do - it 'with dropdown menu' do + + it 'allows user to select unassigned', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - find('.issuable-sidebar'). - select(milestone.title, from: 'issue_milestone_id') - click_button 'Update Issue' - - expect(page).to have_content "Milestone changed to #{milestone.title}" - page.within('.milestone') do - expect(page).to have_content milestone.title + expect(page).to have_content "None" end - has_select?('issue_assignee_id', selected: milestone.title) + find('.block.milestone .edit-link').click + sleep 2 # wait for ajax stuff to complete + first('.dropdown-content li').click + sleep 2 + page.within('.milestone') do + expect(page).to have_content 'None' + end + + expect(issue.reload.milestone).to be_nil end end @@ -283,25 +277,6 @@ describe 'Issues', feature: true do issue.assignee = user2 issue.save end - - it 'allows user to remove assignee', js: true do - visit namespace_project_issue_path(project.namespace, project, issue) - - page.within('.assignee') do - expect(page).to have_content user2.name - end - - find('.assignee .edit-link').click - sleep 2 # wait for ajax stuff to complete - first('.user-result').click - - page.within('.assignee') do - expect(page).to have_content 'None' - end - - sleep 2 # wait for ajax stuff to complete - expect(issue.reload.assignee).to be_nil - end end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 12fd8d3721..3d0d0e59fd 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -39,7 +39,7 @@ describe 'GitLab Markdown', feature: true do end def doc(html = @html) - Nokogiri::HTML::DocumentFragment.parse(html) + @doc ||= Nokogiri::HTML::DocumentFragment.parse(html) end # Shared behavior that all pipelines should exhibit @@ -230,6 +230,7 @@ describe 'GitLab Markdown', feature: true do file = Gollum::File.new(@project_wiki.wiki) expect(file).to receive(:path).and_return('images/example.jpg') expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) + allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb new file mode 100644 index 0000000000..00b60bd0e7 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + project.team << [user, :master] + + login_as user + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'generates a diff for an orphaned branch' do + click_link 'New Merge Request' + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click + + click_button "Compare branches" + click_link "Changes" + + expect(page).to have_content "README.md" + expect(page).to have_content "wm.png" + + fill_in "merge_request_title", with: "Orphaned MR test" + click_button "Submit merge request" + + click_link "Check out branch" + + expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' + end +end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index b76e4c74c7..c57ab5f3b0 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -11,7 +11,41 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) filter_by_milestone(Milestone::None.title) - expect(page).to have_css('.merge-request-title', count: 1) + expect(page).to have_css('.merge-request', count: 1) + end + + context 'filters by upcoming milestone', js: true do + it 'should not show issues with no expiry' do + create(:merge_request, :with_diffs, source_project: project) + create(:merge_request, :simple, source_project: project, milestone: milestone) + + visit_merge_requests(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.merge-request', count: 0) + end + + it 'should show issues in future' do + milestone = create(:milestone, project: project, due_date: Date.tomorrow) + create(:merge_request, :with_diffs, source_project: project) + create(:merge_request, :simple, source_project: project, milestone: milestone) + + visit_merge_requests(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.merge-request', count: 1) + end + + it 'should not show issues in past' do + milestone = create(:milestone, project: project, due_date: Date.yesterday) + create(:merge_request, :with_diffs, source_project: project) + create(:merge_request, :simple, source_project: project, milestone: milestone) + + visit_merge_requests(project) + filter_by_milestone(Milestone::Upcoming.title) + + expect(page).to have_css('.merge-request', count: 0) + end end scenario 'filters by a specific Milestone', js: true do @@ -21,7 +55,7 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) filter_by_milestone(milestone.title) - expect(page).to have_css('.merge-request-title', count: 1) + expect(page).to have_css('.merge-request', count: 1) end def visit_merge_requests(project) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index d9a8058efd..5f855ccc70 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -152,7 +152,7 @@ describe 'Comments', feature: true do it 'has .new_note css class' do page.within('.js-temp-notes-holder') do - expect(subject).to have_css('.new_note') + expect(subject).to have_css('.new-note') end end end @@ -210,7 +210,7 @@ describe 'Comments', feature: true do is_expected.to have_content('Another comment on line 10') is_expected.to have_css('.notes_holder') is_expected.to have_css('.notes_holder .note', count: 1) - is_expected.to have_button('Reply') + is_expected.to have_button('Reply...') end end end @@ -225,6 +225,6 @@ describe 'Comments', feature: true do end def click_diff_line(data = line_code) - page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click + execute_script("$('button[data-line-code=\"#{data}\"]').click()") end end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb new file mode 100644 index 0000000000..13c9b95b31 --- /dev/null +++ b/spec/features/projects/badges/list_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +feature 'list of badges' do + include Select2Helper + + background do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as(user) + visit edit_namespace_project_path(project.namespace, project) + end + + scenario 'user displays list of badges' do + click_link 'Badges' + + expect(page).to have_content 'build status' + expect(page).to have_content 'Markdown' + expect(page).to have_content 'HTML' + expect(page).to have_css('.highlight', count: 2) + expect(page).to have_xpath("//img[@alt='build status']") + + page.within('.highlight', match: :first) do + expect(page).to have_content 'badges/master/build.svg' + end + end + + scenario 'user changes current ref on badges list page', js: true do + click_link 'Badges' + select2('improve/awesome', from: '#ref') + + expect(page).to have_content 'badges/improve/awesome/build.svg' + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 84c036e59c..3e6289a46b 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -1,19 +1,46 @@ require 'spec_helper' describe "Search", feature: true do - before do - login_as :user - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :reporter] - visit search_path + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } - page.within '.search-holder' do - fill_in "search", with: @project.name[0..3] - click_button "Search" + before do + login_with(user) + project.team << [user, :reporter] + visit search_path + end + + describe 'searching for Projects' do + it 'finds a project' do + page.within '.search-holder' do + fill_in "search", with: project.name[0..3] + click_button "Search" + end + + expect(page).to have_content project.name end end - it "should show project in search results" do - expect(page).to have_content @project.name + context 'search for comments' do + it 'finds a snippet' do + snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') + note = create(:note, + noteable: snippet, + author: user, + note: 'Supercalifragilisticexpialidocious', + project: project) + # Must visit project dashboard since global search won't search + # everything (e.g. comments, snippets, etc.) + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: note.note + click_button 'Go' + end + + click_link 'Comments' + + expect(page).to have_link(snippet.title) + end end end diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb new file mode 100644 index 0000000000..db53a9cec9 --- /dev/null +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe "Internal Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :internal) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb new file mode 100644 index 0000000000..d23d645c8e --- /dev/null +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe "Private Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :private) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb new file mode 100644 index 0000000000..e3665b6116 --- /dev/null +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe "Public Project Snippets Access", feature: true do + include AccessMatchers + + let(:project) { create(:project, :public) } + + let(:owner) { project.owner } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + let(:public_snippet) { create(:project_snippet, :public, project: project, author: owner) } + let(:internal_snippet) { create(:project_snippet, :internal, project: project, author: owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: owner) } + + before do + project.team << [master, :master] + project.team << [developer, :developer] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "GET /:project_path/snippets" do + subject { namespace_project_snippets_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets/new" do + subject { new_namespace_project_snippet_path(project.namespace, project) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a public snippet" do + subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + describe "GET /:project_path/snippets/:id for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + describe "GET /:project_path/snippets/:id for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end +end diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml new file mode 100644 index 0000000000..39d5cefbc2 --- /dev/null +++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references.eml @@ -0,0 +1,42 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: reply@appmail.adventuretime.ooo +Message-ID: +In-Reply-To: +References: +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +I could not disagree more. I am obviously biased but adventure time is the +greatest show ever created. Everyone should watch it. + +- Jake out + + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta + wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/fixtures/emails/valid_reply.eml b/spec/fixtures/emails/valid_reply.eml index 1e69638995..980e10a881 100644 --- a/spec/fixtures/emails/valid_reply.eml +++ b/spec/fixtures/emails/valid_reply.eml @@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400 From: Jake the Dog To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo Message-ID: +In-Reply-To: +References: Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' Mime-Version: 1.0 Content-Type: text/plain; @@ -37,4 +39,4 @@ On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta > Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 > > To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). -> \ No newline at end of file +> diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb new file mode 100644 index 0000000000..b20373a96f --- /dev/null +++ b/spec/helpers/form_helper_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe FormHelper do + describe 'form_errors' do + it 'returns nil when model has no errors' do + model = double(errors: []) + + expect(helper.form_errors(model)).to be_nil + end + + it 'renders an alert div' do + model = double(errors: errors_stub('Error 1')) + + expect(helper.form_errors(model)). + to include('
    ') + end + + it 'contains a summary message' do + single_error = double(errors: errors_stub('A')) + multi_errors = double(errors: errors_stub('A', 'B', 'C')) + + expect(helper.form_errors(single_error)). + to include('

    The form contains the following error:') + expect(helper.form_errors(multi_errors)). + to include('

    The form contains the following errors:') + end + + it 'renders each message' do + model = double(errors: errors_stub('Error 1', 'Error 2', 'Error 3')) + + errors = helper.form_errors(model) + + aggregate_failures do + expect(errors).to include('
  • Error 1
  • ') + expect(errors).to include('
  • Error 2
  • ') + expect(errors).to include('
  • Error 3
  • ') + end + end + + def errors_stub(*messages) + ActiveModel::Errors.new(double).tap do |errors| + messages.each { |msg| errors.add(:base, msg) } + end + end + end +end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9adcd916ce..13de88e2f2 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -150,13 +150,6 @@ describe GitlabMarkdownHelper do end end - describe 'random_markdown_tip' do - it 'returns a random Markdown tip' do - stub_const("#{described_class}::MARKDOWN_TIPS", ['Random tip']) - expect(random_markdown_tip).to eq 'Random tip' - end - end - describe '#first_line_in_markdown' do let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ffd8ebae02..543593cf38 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -80,7 +80,7 @@ describe IssuesHelper do end end - describe '#url_for_new_issue' do + describe 'url_for_new_issue' do let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } let(:ext_expected) do issues_url.gsub(':project_id', ext_project.id.to_s) @@ -117,7 +117,7 @@ describe IssuesHelper do end end - describe "#merge_requests_sentence" do + describe "merge_requests_sentence" do subject { merge_requests_sentence(merge_requests)} let(:merge_requests) do [ build(:merge_request, iid: 1), build(:merge_request, iid: 2), @@ -127,7 +127,7 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "#note_active_class" do + describe "note_active_class" do before do @note = create :note @note1 = create :note @@ -142,10 +142,25 @@ describe IssuesHelper do end end - describe "#awards_sort" do + 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 + + describe "milestone_options" do + it "gets closed milestone from current issue" do + closed_milestone = create(:closed_milestone, project: project) + milestone1 = create(:milestone, project: project) + milestone2 = create(:milestone, project: project) + issue.update_attributes(milestone_id: closed_milestone.id) + + options = milestone_options(issue) + + expect(options).to have_selector('option[selected]', text: closed_milestone.title) + expect(options).to have_selector('option', text: milestone1.title) + expect(options).to have_selector('option', text: milestone2.title) + end + end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index e5df59c4fb..2f9291afc3 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -19,7 +19,9 @@ describe PreferencesHelper do ['Your Projects (default)', 'projects'], ['Starred Projects', 'stars'], ["Your Projects' Activity", 'project_activity'], - ["Starred Projects' Activity", 'starred_project_activity'] + ["Starred Projects' Activity", 'starred_project_activity'], + ["Your Groups", 'groups'], + ["Your Todos", 'todos'] ] end end diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml index 1701652c61..cb906a7fea 100644 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -1,4 +1,4 @@ -.zennable +.md-area .zen-backdrop %textarea#note_note.js-gfm-input.markdown-area %a.js-zen-enter(tabindex="-1" href="#") diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 86ba9dd8e9..ea27f36e9b 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -29,8 +29,8 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success saved: true - + req.success id: 34 + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') expect($btnReopen).toBeHidden() @@ -94,7 +94,7 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success saved: true + req.success id: 34 $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb new file mode 100644 index 0000000000..330678f7f1 --- /dev/null +++ b/spec/lib/award_emoji_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe AwardEmoji do + describe '.urls' do + subject { AwardEmoji.urls } + + it { is_expected.to be_an_instance_of(Array) } + it { is_expected.to_not be_empty } + + context 'every Hash in the Array' do + it 'has the correct keys and values' do + subject.each do |hash| + expect(hash[:name]).to be_an_instance_of(String) + expect(hash[:path]).to be_an_instance_of(String) + end + end + end + end +end diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index 5e23c5c319..fe2ce092e6 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -70,20 +70,22 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do end context 'linking internal resources' do - it "the created link's text will be equal to the resource's text" do + it "the created link's text includes the resource's text and wiki base path" do tag = '[[wiki-slug]]' doc = filter("See #{tag}", project_wiki: project_wiki) + expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug') expect(doc.at_css('a').text).to eq 'wiki-slug' - expect(doc.at_css('a')['href']).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq expected_path end it "the created link's text will be link-text" do tag = '[[link-text|wiki-slug]]' doc = filter("See #{tag}", project_wiki: project_wiki) + expected_path = ::File.join(project_wiki.wiki_base_path, 'wiki-slug') expect(doc.at_css('a').text).to eq 'link-text' - expect(doc.at_css('a')['href']).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq expected_path end end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb new file mode 100644 index 0000000000..dd5594750c --- /dev/null +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Banzai::Filter::ImageLinkFilter, lib: true do + include FilterSpecHelper + + def image(path) + %() + end + + it 'wraps the image with a link to the image src' do + doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end + + it 'does not wrap a duplicate link' do + exp = act = %q(
    #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}) + expect(filter(act).to_html).to eq exp + end + + it 'works with external images' do + doc = filter(image('https://i.imgur.com/DfssX9C.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 5a0d3d577a..266ebef33d 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do result = reference_pipeline_result("Fixed #{reference}") expect(result[:references][:issue]).to eq [issue] end + + it 'does not process links containing issue numbers followed by text' do + href = "#{reference}st" + doc = reference_filter("") + link = doc.css('a').first.attr('href') + + expect(link).to eq(href) + end end context 'cross-project reference' do diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 3e25406e49..7aa1b4a3bf 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -11,7 +11,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - result = described_class.call(markdown, project: spy, project_wiki: double) + result = described_class.call(markdown, project: spy, project_wiki: spy) aggregate_failures do expect(result[:output].text).not_to include '[[' @@ -29,7 +29,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - output = described_class.to_html(markdown, project: spy, project_wiki: double) + output = described_class.to_html(markdown, project: spy, project_wiki: spy) expect(output).to include('[[toc]]') end @@ -42,7 +42,7 @@ describe Banzai::Pipeline::WikiPipeline do Foo MD - output = described_class.to_html(markdown, project: spy, project_wiki: double) + output = described_class.to_html(markdown, project: spy, project_wiki: spy) aggregate_failures do expect(output).not_to include('
      ') diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index f38fadda9b..566035c60d 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers let(:project) { double('project') } diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb new file mode 100644 index 0000000000..329792bb68 --- /dev/null +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build do + let(:project) { create(:project) } + let(:sha) { project.commit.sha } + let(:branch) { 'master' } + let(:badge) { described_class.new(project, branch) } + + describe '#type' do + subject { badge.type } + it { is_expected.to eq 'image/svg+xml' } + end + + describe '#to_html' do + let(:html) { Nokogiri::HTML.parse(badge.to_html) } + let(:a_href) { html.at('a') } + + it 'points to link' do + expect(a_href[:href]).to eq badge.link_url + end + + it 'contains clickable image' do + expect(a_href.children.first.name).to eq 'img' + end + end + + describe '#to_markdown' do + subject { badge.to_markdown } + + it { is_expected.to include badge.image_url } + it { is_expected.to include badge.link_url } + end + + describe '#image_url' do + subject { badge.image_url } + it { is_expected.to include "badges/#{branch}/build.svg" } + end + + describe '#link_url' do + subject { badge.link_url } + it { is_expected.to include "commits/#{branch}" } + end + + context 'build exists' do + let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } + let!(:build) { create(:ci_build, commit: ci_commit) } + + + context 'build success' do + before { build.success! } + + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-success' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about success' do + expect(status_node(data, 'success')).to be_truthy + end + end + end + + context 'build failed' do + before { build.drop! } + + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-failed' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about failure' do + expect(status_node(data, 'failed')).to be_truthy + end + end + end + end + + context 'build does not exist' do + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-unknown' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about unknown build' do + expect(status_node(data, 'unknown')).to be_truthy + end + end + end + + def status_node(data, status) + xml = Nokogiri::XML.parse(data) + xml.at(%Q{text:contains("#{status}")}) + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 844fd79c99..e9b8ce6b5b 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -22,11 +22,21 @@ describe Gitlab::ClosingIssueExtractor, lib: true do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Closes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (closes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) @@ -37,106 +47,211 @@ describe Gitlab::ClosingIssueExtractor, lib: true do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "closed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Closing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "closing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Close: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "close: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (Fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Fixes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Fixes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fixed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fixed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fixing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fixing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fix: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fix: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (Resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Resolves: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (resolves: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolved: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolved: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolving: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolving: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolve: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolve: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') @@ -236,6 +351,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index abe179cd4a..36267faeb9 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe Gitlab::Email::Receiver, lib: true do before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') end let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } @@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do expect(note.note).to include(markdown) end + + context 'when sub-addressing is not supported' do + before do + stub_incoming_email_setting(enabled: true, address: nil) + end + + shared_examples 'an email that contains a reply key' do |header| + it "fetches the reply key from the #{header} header and creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include('I could not disagree more.') + end + end + + context 'reply key is in the References header' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } + + it_behaves_like 'an email that contains a reply key', 'References' + end + end end end diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb new file mode 100644 index 0000000000..2dc71be025 --- /dev/null +++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::FogbugzImport::Client, lib: true do + + let(:client) { described_class.new(uri: '', token: '') } + let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } } + let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } } + + it 'retrieves user_map with one user' do + stub_api(one_user) + + expect(client.user_map.count).to eq(1) + end + + it 'retrieves user_map with two users' do + stub_api(two_users) + + expect(client.user_map.count).to eq(2) + end + + def stub_api(users) + allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users) + end +end diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb new file mode 100644 index 0000000000..eda956e6f0 --- /dev/null +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::Gfm::UploadsRewriter do + let(:user) { create(:user) } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + let(:rewriter) { described_class.new(text, old_project, user) } + + context 'text contains links to uploads' do + let(:image_uploader) do + build(:file_uploader, project: old_project) + end + + let(:zip_uploader) do + build(:file_uploader, project: old_project, + fixture: 'ci_build_artifacts.zip') + end + + let(:text) do + "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}" + end + + describe '#rewrite' do + let!(:new_text) { rewriter.rewrite(new_project) } + + let(:old_files) { [image_uploader, zip_uploader].map(&:file) } + let(:new_files) do + described_class.new(new_text, new_project, user).files + end + + let(:old_paths) { old_files.map(&:path) } + let(:new_paths) { new_files.map(&:path) } + + it 'rewrites content' do + expect(new_text).to_not eq text + expect(new_text.length).to eq text.length + end + + it 'copies files' do + expect(new_files).to all(exist) + expect(old_paths).to_not match_array new_paths + expect(old_paths).to all(include(old_project.path_with_namespace)) + expect(new_paths).to all(include(new_project.path_with_namespace)) + end + + it 'does not remove old files' do + expect(old_files).to all(exist) + end + + it 'generates a new secret for each file' do + expect(new_paths).to_not include image_uploader.secret + expect(new_paths).to_not include zip_uploader.secret + end + end + + describe '#needs_rewrite?' do + subject { rewriter.needs_rewrite? } + it { is_expected.to eq true } + end + + describe '#files' do + subject { rewriter.files } + it { is_expected.to be_an(Array) } + end + end +end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index bcdba8d4c1..afb3e26f8f 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do stub_incoming_email_setting(enabled: true) end - context "when the address is valid" do - before do - stub_incoming_email_setting(address: "replies+%{key}@example.com") - end - - it "returns true" do - expect(described_class.enabled?).to be_truthy - end - end - - context "when the address is invalid" do - before do - stub_incoming_email_setting(address: "replies@example.com") - end - - it "returns false" do - expect(described_class.enabled?).to be_falsey - end + it 'returns true' do + expect(described_class.enabled?).to be_truthy end end @@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do expect(described_class.key_from_address("replies+key@example.com")).to eq("key") end end + + context 'self.key_from_fallback_reply_message_id' do + it 'returns reply key' do + expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key') + end + end end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 32a19bf344..f5b66b8156 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do access.allowed? expect(user).to be_blocked expect(user).to be_ldap_blocked @@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_truthy } + + context 'when user cannot be found' do + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + end + + it { is_expected.to be_falsey } + + it 'blocks user in GitLab' do + access.allowed? + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + end + + context 'when user was previously ldap_blocked' do + before do + user.ldap_block + end + + it 'unblocks the user if it exists' do + access.allowed? + expect(user).not_to be_blocked + end + end end end end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb new file mode 100644 index 0000000000..e01b0b4bd2 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Subscribers::RailsCache do + let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:subscriber) { described_class.new } + + let(:event) { double(:event, duration: 15.2) } + + describe '#cache_read' do + it 'increments the cache_read duration' do + expect(subscriber).to receive(:increment). + with(:cache_read_duration, event.duration) + + subscriber.cache_read(event) + end + end + + describe '#cache_write' do + it 'increments the cache_write duration' do + expect(subscriber).to receive(:increment). + with(:cache_write_duration, event.duration) + + subscriber.cache_write(event) + end + end + + describe '#cache_delete' do + it 'increments the cache_delete duration' do + expect(subscriber).to receive(:increment). + with(:cache_delete_duration, event.duration) + + subscriber.cache_delete(event) + end + end + + describe '#cache_exist?' do + it 'increments the cache_exists duration' do + expect(subscriber).to receive(:increment). + with(:cache_exists_duration, event.duration) + + subscriber.cache_exist?(event) + end + end + + describe '#increment' do + context 'without a transaction' do + it 'returns' do + expect(transaction).not_to receive(:increment) + + subscriber.increment(:foo, 15.2) + end + end + + context 'with a transaction' do + before do + allow(subscriber).to receive(:current_transaction). + and_return(transaction) + end + + it 'increments the total and specific cache duration' do + expect(transaction).to receive(:increment). + with(:cache_duration, event.duration) + + expect(transaction).to receive(:increment). + with(:cache_delete_duration, event.duration) + + subscriber.increment(:cache_delete_duration, event.duration) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index f8c1d956ca..d6ae54e25e 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -26,4 +26,10 @@ describe Gitlab::Metrics::System do end end end + + describe '.cpu_time' do + it 'returns a Fixnum' do + expect(described_class.cpu_time).to be_an_instance_of(Fixnum) + end + end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 0ec8a6dc5c..3dee13e27f 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Metrics do end end - describe '#submit_metrics' do + describe '.submit_metrics' do it 'prepares and writes the metrics to InfluxDB' do connection = double(:connection) pool = double(:pool) @@ -26,7 +26,7 @@ describe Gitlab::Metrics do end end - describe '#prepare_metrics' do + describe '.prepare_metrics' do it 'returns a Hash with the keys as Symbols' do metrics = described_class. prepare_metrics([{ 'values' => {}, 'tags' => {} }]) @@ -51,7 +51,7 @@ describe Gitlab::Metrics do end end - describe '#escape_value' do + describe '.escape_value' do it 'escapes an equals sign' do expect(described_class.escape_value('foo=')).to eq('foo\\=') end @@ -60,4 +60,42 @@ describe Gitlab::Metrics do expect(described_class.escape_value(10)).to eq('10') end end + + describe '.measure' do + context 'without a transaction' do + it 'returns the return value of the block' do + val = Gitlab::Metrics.measure(:foo) { 10 } + + expect(val).to eq(10) + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + before do + allow(Gitlab::Metrics).to receive(:current_transaction). + and_return(transaction) + end + + it 'adds a metric to the current transaction' do + expect(transaction).to receive(:increment). + with('foo_real_time', a_kind_of(Numeric)) + + expect(transaction).to receive(:increment). + with('foo_cpu_time', a_kind_of(Numeric)) + + expect(transaction).to receive(:increment). + with('foo_call_count', 1) + + Gitlab::Metrics.measure(:foo) { 10 } + end + + it 'returns the return value of the block' do + val = Gitlab::Metrics.measure(:foo) { 10 } + + expect(val).to eq(10) + end + end + end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index de7cd99d49..c2a51d9249 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Saml::User, lib: true do let(:gl_user) { saml_user.gl_user } let(:uid) { 'my-uid' } let(:provider) { 'saml' } - let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) } let(:info_hash) do { name: 'John', @@ -23,10 +23,20 @@ describe Gitlab::Saml::User, lib: true do allow(Gitlab::LDAP::Config).to receive_messages(messages) end + def stub_basic_saml_config + allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } }) + end + + def stub_saml_group_config(groups) + allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) + end + + before { stub_basic_saml_config } + describe 'account exists on server' do before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } context 'and should bind with SAML' do - let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } it 'adds the SAML identity to the existing user' do saml_user.save expect(gl_user).to be_valid @@ -36,6 +46,35 @@ describe Gitlab::Saml::User, lib: true do expect(identity.provider).to eql 'saml' end end + + context 'external groups' do + context 'are defined' do + it 'marks the user as external' do + stub_saml_group_config(%w(Freelancers)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + before { stub_saml_group_config(%w(Interns)) } + context 'are defined but the user does not belong there' do + it 'does not mark the user as external' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + + context 'user was external, now should not be' do + it 'should make user internal' do + existing_user.update_attribute('external', true) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + end end describe 'no account exists on server' do @@ -68,6 +107,26 @@ describe Gitlab::Saml::User, lib: true do end end + context 'external groups' do + context 'are defined' do + it 'marks the user as external' do + stub_saml_group_config(%w(Freelancers)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + context 'are defined but the user does not belong there' do + it 'does not mark the user as external' do + stub_saml_group_config(%w(Interns)) + saml_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + end + context 'with auto_link_ldap_user disabled (default)' do before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } include_examples 'to verify compliance with allow_single_sign_on' @@ -76,12 +135,6 @@ describe Gitlab::Saml::User, lib: true do context 'with auto_link_ldap_user enabled' do before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } - context 'and no LDAP provider defined' do - before { stub_ldap_config(providers: []) } - - include_examples 'to verify compliance with allow_single_sign_on' - end - context 'and at least one LDAP provider is defined' do before { stub_ldap_config(providers: %w(ldapmain)) } @@ -89,19 +142,18 @@ describe Gitlab::Saml::User, lib: true do before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } - allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context 'and no account for the LDAP user' do - it 'creates a user with dual LDAP and SAML identities' do saml_user.save expect(gl_user).to be_valid expect(gl_user.username).to eql uid - expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to eql 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, @@ -111,13 +163,13 @@ describe Gitlab::Saml::User, lib: true do end context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } - it "adds the omniauth identity to the LDAP account" do + let!(:existing_user) { create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it 'adds the omniauth identity to the LDAP account' do saml_user.save expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' - expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to eql 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, @@ -126,19 +178,13 @@ describe Gitlab::Saml::User, lib: true do end end end - - context 'and no corresponding LDAP person' do - before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } - - include_examples 'to verify compliance with allow_single_sign_on' - end end end end describe 'blocking' do - before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } + before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } context 'signup with SAML only' do context 'dont block on create' do @@ -162,64 +208,6 @@ describe Gitlab::Saml::User, lib: true do end end - context 'signup with linked omniauth and LDAP account' do - before do - stub_omniauth_config(auto_link_ldap_user: true) - allow(ldap_user).to receive(:uid) { uid } - allow(ldap_user).to receive(:username) { uid } - allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(saml_user).to receive(:ldap_person).and_return(ldap_user) - end - - context "and no account for the LDAP user" do - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).to be_blocked - end - end - end - - context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } - - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - end - end - - context 'sign-in' do before do saml_user.save @@ -245,26 +233,6 @@ describe Gitlab::Saml::User, lib: true do expect(gl_user).not_to be_blocked end end - - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 9b47acfe0c..631b5094f4 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -35,7 +35,9 @@ describe Notify do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } it_behaves_like 'an assignee email' - it_behaves_like 'an email starting a new thread', 'issue' + it_behaves_like 'an email starting a new thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -73,9 +75,11 @@ describe Notify do subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -104,7 +108,9 @@ describe Notify do subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'an email with a labels subscriptions link in its footer' @@ -132,7 +138,9 @@ describe Notify do let(:status) { 'closed' } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -163,7 +171,9 @@ describe Notify do let(:new_issue) { create(:issue) } subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -196,9 +206,11 @@ describe Notify do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } it_behaves_like 'an assignee email' - it_behaves_like 'an email starting a new thread', 'merge_request' + it_behaves_like 'an email starting a new thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -216,10 +228,6 @@ describe Notify do is_expected.to have_body_text /#{merge_request.target_branch}/ end - it 'has the correct message-id set' do - is_expected.to have_header 'Message-ID', "" - end - context 'when enabled email_author_in_body' do before do allow(current_application_settings).to receive(:email_author_in_body).and_return(true) @@ -247,7 +255,9 @@ describe Notify do subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like "an unsubscribeable thread" @@ -278,7 +288,9 @@ describe Notify do subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'an email with a labels subscriptions link in its footer' @@ -306,9 +318,11 @@ describe Notify do let(:status) { 'reopened' } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -337,9 +351,11 @@ describe Notify do subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -456,9 +472,11 @@ describe Notify do subject { Notify.note_commit_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'commit' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { commit } + end it_behaves_like 'it should show Gmail Actions View Commit link' - it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'a user cannot unsubscribe through footer link' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -477,7 +495,9 @@ describe Notify do subject { Notify.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' @@ -498,7 +518,9 @@ describe Notify do subject { Notify.note_issue_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 6019af544d..5a85cb501d 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do ActionMailer::Base.deliveries.clear email = recipient.emails.create(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) + stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}") + end +end + +shared_context 'reply-by-email is enabled with incoming address without %{key}' do + before do + stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}") end end @@ -46,25 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do end end -shared_examples 'an email starting a new thread' do |message_id_prefix| - include_examples 'an email with X-GitLab headers containing project details' +shared_examples 'a new thread email with reply-by-email enabled' do + let(:regex) { /\A\Z/ } - it 'has a discussion identifier' do - is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + it 'has a Message-ID header' do + is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" + end + + it 'has a References header' do + is_expected.to have_header 'References', regex end end -shared_examples 'an answer to an existing thread' do |thread_id_prefix| +shared_examples 'a thread answer email with reply-by-email enabled' do include_examples 'an email with X-GitLab headers containing project details' + let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> \Z/ } + + it 'has a Message-ID header' do + is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/ + end + + it 'has a In-Reply-To header' do + is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>" + end + + it 'has a References header' do + is_expected.to have_header 'References', regex + end it 'has a subject that begins with Re: ' do is_expected.to have_subject /^Re: / end +end - it 'has headers that reference an existing thread' do - is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ +shared_examples 'an email starting a new thread with reply-by-email enabled' do + include_examples 'an email with X-GitLab headers containing project details' + include_examples 'a new thread email with reply-by-email enabled' + + context 'when reply-by-email is enabled with incoming address with %{key}' do + it 'has a Reply-To header' do + is_expected.to have_header 'Reply-To', /\Z/ + end + end + + context 'when reply-by-email is enabled with incoming address without %{key}' do + include_context 'reply-by-email is enabled with incoming address without %{key}' + include_examples 'a new thread email with reply-by-email enabled' + + it 'has a Reply-To header' do + is_expected.to have_header 'Reply-To', /\Z/ + end + end +end + +shared_examples 'an answer to an existing thread with reply-by-email enabled' do + include_examples 'an email with X-GitLab headers containing project details' + include_examples 'a thread answer email with reply-by-email enabled' + + context 'when reply-by-email is enabled with incoming address with %{key}' do + it 'has a Reply-To header' do + is_expected.to have_header 'Reply-To', /\Z/ + end + end + + context 'when reply-by-email is enabled with incoming address without %{key}' do + include_context 'reply-by-email is enabled with incoming address without %{key}' + include_examples 'a thread answer email with reply-by-email enabled' + + it 'has a Reply-To header' do + is_expected.to have_header 'Reply-To', /\Z/ + end end end @@ -83,10 +141,12 @@ shared_examples 'a new user email' do end shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text '