diff --git a/.scss-lint.yml b/.scss-lint.yml index 937d3407b60..3ce0c4901bd 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -132,8 +132,9 @@ linters: SpaceAroundOperator: enabled: false + # Opening braces should be preceded by a single space. SpaceBeforeBrace: - enabled: false + enabled: true StringQuotes: enabled: false diff --git a/CHANGELOG b/CHANGELOG index 7e9a447a8f6..f72bb670ece 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,16 +1,61 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - 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) - 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 avatar stretching by providing a cropping feature + - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages + - Handle nil descriptions in Slack issue messages (Stan Hu) + - Add default scope to projects to exclude projects pending deletion - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - 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) + - 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) -v 8.6.2 (unreleased) - - Comments on confidential issues don't show up in activity feed to non-members +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) + +v 8.6.3 + - Mentions on confidential issues doesn't create todos for non-members. !3374 + - Destroy related todos when an Issue/MR is deleted. !3376 + - Fix error 500 when target is nil on todo list. !3376 + - Fix copying uploads when moving issue to another project. !3382 + - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432 + - Fix raw/rendered diff producing different results on merge requests. !3450 + - Fix commit comment alignment (Stan Hu). !3466 + - Fix Error 500 when searching for a comment in a project snippet. !3468 + - Allow temporary email as notification email. !3477 + - Fix issue with dropdowns not selecting values. !3478 + - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280 + +v 8.6.2 + - Fix dropdown alignment. !3298 + - Fix issuable sidebar overlaps on tablet. !3299 + - Make dropdowns pixel perfect. !3337 + - Fix order of steps to prevent PostgreSQL errors when running migration. !3355 + - Fix bold text in issuable sidebar. !3358 + - Fix error with anonymous token in applications settings. !3362 + - Fix the milestone 'upcoming' filter. !3364 + !3368 + - Fix comments on confidential issues showing up in activity feed to non-members. !3375 + - Fix `NoMethodError` when visiting CI root path at `/ci`. !3377 + - Add a tooltip to new branch button in issue page. !3380 + - Fix an issue hiding the password form when signed-in with a linked account. !3381 + - Add links to CI setup documentation from project settings and builds pages. !3384 + - Fix an issue with width of project select dropdown. !3386 + - Remove redundant `require`s from Banzai files. !3391 + - Fix error 500 with cancel button on issuable edit form. !3392 + !3417 + - Fix background when editing a highlighted note. !3423 + - Remove tabstop from the WIP toggle links. !3426 + - Ensure private project snippets are not viewable by unauthorized people. + - Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402 + - Fixed issue with notification settings not saving. !3452 v 8.6.1 - Add option to reload the schema before restoring a database backup. !2807 @@ -83,6 +128,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 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index bc02b8685c1..24ba9a38de6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.11 +2.7.0 diff --git a/Gemfile b/Gemfile index 006e53e0c10..6327227282a 100644 --- a/Gemfile +++ b/Gemfile @@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store', '~> 1.2.0' +gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index da27c62acbf..229089f431d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,9 +126,9 @@ GEM coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (4.1.0) + coffee-rails (4.1.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0, < 5.1.x) coffee-script (2.4.1) coffee-script-source execjs @@ -652,7 +652,7 @@ 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) @@ -1011,7 +1011,7 @@ DEPENDENCIES redcarpet (~> 3.3.3) redis-namespace redis-rails (~> 4.0.0) - request_store (~> 1.2.0) + request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f5e1ca9860d..70fd6f50e9c 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -146,15 +146,11 @@ class Dispatcher when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() - # If we haven't installed a custom shortcut handler, install the default one if not shortcut_handler new Shortcuts() initSearch: -> - opts = $('.search-autocomplete-opts') - path = opts.data('autocomplete-path') - project_id = opts.data('autocomplete-project-id') - project_ref = opts.data('autocomplete-project-ref') - new SearchAutocomplete(path, project_id, project_ref) + # Only when search form is present + new SearchAutocomplete() if $('.search').length diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4b78bcde774..4f032a82e58 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -3,6 +3,10 @@ class GitLabDropdownFilter HAS_VALUE_CLASS = "has-value" constructor: (@input, @options) -> + { + @filterInputBlur = true + } = @options + $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') @@ -33,7 +37,7 @@ class GitLabDropdownFilter blur_field = @shouldBlur e.keyCode search_text = @input.val() - if blur_field + if blur_field and @filterInputBlur @input.blur() if @options.remote @@ -93,27 +97,48 @@ class GitLabDropdown PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + FILTER_INPUT = '.dropdown-input .dropdown-input-field' + constructor: (@el, @options) -> - self = @ @dropdown = $(@el).parent() + + # Set Defaults + { + # If no input is passed create a default one + @filterInput = @getElement(FILTER_INPUT) + @highlight = false + @filterInputBlur = true + @enterCallback = true + } = @options + + self = @ + + # If selector was passed + if _.isString(@filterInput) + @filterInput = @getElement(@filterInput) + search_fields = if @options.search then @options.search.fields else []; if @options.data - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data + # If data is an array + if _.isArray @options.data + @fullData = @options.data + @parseData @options.data + else + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data - @parseData @fullData - } + @parseData @fullData + } - # Init filiterable + # Init filterable if @options.filterable - @input = @dropdown.find('.dropdown-input .dropdown-input-field') - - @filter = new GitLabDropdownFilter @input, + @filter = new GitLabDropdownFilter @filterInput, + filterInputBlur: @filterInputBlur remote: @options.filterRemote query: @options.data keys: @options.search.fields @@ -123,11 +148,14 @@ class GitLabDropdown @parseData data @highlightRow 1 enterCallback: => - @selectFirstRow() + if @enterCallback + @selectFirstRow() # 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) => @@ -143,10 +171,14 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> - self.rowClicked $(@) + selected = self.rowClicked $(@) if self.options.clicked - self.options.clicked() + self.options.clicked(selected) + + # Finds an element inside wrapper element + getElement: (selector) -> + @dropdown.find selector toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -176,15 +208,26 @@ 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: => 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) => if @options.filterable @dropdown .find(".dropdown-input-field") @@ -195,6 +238,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) -> @@ -219,20 +267,46 @@ 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" + if @highlight + text = @highlightTextMatches(text, @filterInput.val()) + html = "
  • " html += "" html += text @@ -241,58 +315,68 @@ class GitLabDropdown 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 += "" html += "No matching results." html += "" html += "
  • " highlightRow: (index) -> - if @input.val() isnt "" + if @filterInput.val() isnt "" selector = '.dropdown-content li:first-child a' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content li:first-child a" - $(selector).addClass 'is-focused' + @getElement(selector).addClass 'is-focused' rowClicked: (el) -> fieldName = @options.fieldName - 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 + # Toggle the dropdown label + if @options.toggleLabel + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + else if !value? field.remove() - if @options.multiSelect - oldValue = field.val() - if oldValue - value = "#{oldValue},#{value}" - else + 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.toggleClass "is-active" + el.addClass ACTIVE_CLASS # Toggle the dropdown label if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) - if value? if !field.length # 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 selectFirstRow: -> selector = '.dropdown-content li:first-child a' @@ -304,4 +388,6 @@ class GitLabDropdown $.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 d6d09b36d8d..2f19513a831 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/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 1127b289264..b1479bfb449 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) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index e08648d583b..d1fe116397a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -4,14 +4,21 @@ class @LabelsSelect $dropdown = $(dropdown) projectId = $dropdown.data('project-id') labelUrl = $dropdown.data('labels') + issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel - selectedLabel = selectedLabel.toString().split(',') + 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') @@ -21,6 +28,22 @@ class @LabelsSelect # 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 @@ -57,6 +80,23 @@ class @LabelsSelect # 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() + + # 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' + $newLabelCreateButton.enable() else $newLabelCreateButton.disable() @@ -90,41 +130,84 @@ class @LabelsSelect 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} @@ -133,6 +216,7 @@ class @LabelsSelect search: fields: ['title'] selectable: true + toggleLabel: (selected) -> if selected and selected.title isnt 'Any Label' selected.title @@ -142,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 00000000000..8f892b5a2b9 --- /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 00000000000..3f9ca39912c --- /dev/null +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -0,0 +1,30 @@ +((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick + + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> + opts = + body: body + icon: icon + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notificationGranted message, opts, onclick + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notificationGranted message, opts, onclick + + w.notify = notifyMe + w.notifyPermissions = notifyPermissions +) window diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343b..7102a0673e9 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -2,13 +2,18 @@ class @MergeRequestWidget # Initialize MergeRequestWidget behavior # # check_enable - Boolean, whether to check automerge status - # url_to_automerge_check - String, URL to use to check automerge status - # current_status - String, current automerge status - # ci_enable - Boolean, whether a CI service is enabled - # url_to_ci_check - String, URL to use to check CI status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status # + constructor: (@opts) -> - modal = $('#modal_merge_info').modal(show: false) + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = true + clearInterval @fetchBuildStatusInterval + + @pollCIStatus() + notifyPermissions() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -27,18 +32,57 @@ 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 == 'success' + 'passed' + else + status - showCiState: (state) -> + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 5000 + + 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 data.status isnt @opts.ci_status + @showCIStatus data.status + if data.coverage + @showCICoverage data.coverage + + if showNotification + message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + "Build #{@ciLabelForStatus(data.status)}", + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + + @opts.ci_status = data.status + + showCIStatus: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -52,7 +96,7 @@ class @MergeRequestWidget $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') - showCiCoverage: (coverage) -> + showCICoverage: (coverage) -> text = 'Coverage ' + coverage + '%' $('.ci_widget:visible .ci-coverage').text(text) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index e17a1adb648..f73127f49f0 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,51 @@ 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: -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' + milestone.name is selectedMilestone + hidden: -> + $selectbox.hide() - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + clicked: (selected) -> + if $dropdown.hasClass 'js-filter-bulk-update' + return + + if $dropdown.hasClass('js-filter-submit') + if selected.name? + selectedMilestone = selected.name + else + selectedMilestone = '' Issues.filterResults $dropdown.closest('form') - 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/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee new file mode 100644 index 00000000000..67403554340 --- /dev/null +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -0,0 +1,55 @@ +class @Sidebar + constructor: (currentUser) -> + @addEventListeners() + + addEventListeners: -> + $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked) + $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden) + $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) + $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) + + sidebarDropdownLoading: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + i = $sidebarCollapsedIcon.find('i') + $loading = $('') + if img.length + img.before($loading) + img.hide() + else if i.length + i.before($loading) + i.hide() + + sidebarDropdownLoaded: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + $sidebarCollapsedIcon.find('i.fa-spin').remove() + i = $sidebarCollapsedIcon.find('i') + if img.length + img.show() + else + i.show() + + + sidebarCollapseClicked: (e) -> + e.preventDefault() + $block = $(@).closest('.block') + + $('aside') + .find('.gutter-toggle') + .trigger('click') + $editLink = $block.find('.edit-link') + + if $editLink.length + $editLink.trigger('click') + $block.addClass('collapse-after-update') + $('.page-with-sidebar').addClass('with-overlay') + + sidebarDropdownHidden: (e) -> + $block = $(@).closest('.block') + if $block.hasClass('collapse-after-update') + $block.removeClass('collapse-after-update') + $('.page-with-sidebar').removeClass('with-overlay') + $('aside') + .find('.gutter-toggle') + .trigger('click') \ No newline at end of file diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index c1801365266..030655491bf 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,11 +1,270 @@ 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(@) + + 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 + 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: -> + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'keyup', @onSearchInputKeyUp + @searchInput.on 'click', @onSearchInputClick + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur + @clearInput.on 'click', @onRemoveLocationClick + + 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() + + # Avoid falsy value to be returned + return + + onSearchInputClick: (e) => + # Prevents closing the dropdown menu + e.stopImmediatePropagation() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onRemoveLocationClick: (e) => + e.preventDefault() + @removeLocationBadge() + @searchInput.val('').focus() + @skipBlurEvent = true + + onSearchInputBlur: (e) => + @skipBlurEvent = false + + # We should wait to make sure we are not clearing the input instead + setTimeout( => + return if @skipBlurEvent + + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + , 150) + + 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) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d0..e1778511240 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") - $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) setTimeout ( -> diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index b6b4bd90e6a..ec2df6c5b73 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,6 @@ class @Todos updateBadges: (data) -> $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + + goToTodoUrl: -> + Turbolinks.visit($(this).data('url')) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 84193400890..eee9b6e690e 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) => @@ -57,20 +134,38 @@ 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 @@ -87,17 +182,25 @@ class @UsersSelect if avatar img = "" - "
  • - - #{img} - - #{user.name} - - - #{username} - - -
  • " + # 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/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e2d590f4df4..69b3b6586de 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ *= 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 469f4f296ae..542a53f0377 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -13,10 +13,10 @@ // Toggle between two states. .js-toggler-container { - .turn-on { display: block; } + .turn-on { display: block; } .turn-off { display: none; } &.on { - .turn-on { display: none; } + .turn-on { display: none; } .turn-off { display: block; } } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 9b676d759e0..db1a8b1bf78 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -121,7 +121,7 @@ p.time { text-shadow: none; } -.thin_area{ +.thin_area { height: 150px; } @@ -148,7 +148,7 @@ li.note { } } -.wiki_content code, .readme code{ +.wiki_content code, .readme code { background-color: inherit; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2d616fc660c..82dc1acbd01 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,7 +42,7 @@ font-size: 15px; text-align: left; border: 1px solid $dropdown-toggle-border-color; - border-radius: 2px; + border-radius: $dropdown-border-radius; outline: 0; text-overflow: ellipsis; white-space: nowrap; @@ -75,12 +75,12 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - padding: 10px; - font-size: 14px; + font-size: 15px; font-weight: normal; + padding: 10px 0; background-color: $dropdown-bg; border: 1px solid $dropdown-border-color; - border-radius: $border-radius-base; + border-radius: $dropdown-border-radius; box-shadow: 0 2px 4px $dropdown-shadow-color; &.is-loading { @@ -101,9 +101,17 @@ li { text-align: left; list-style: none; + padding: 0 10px; } .divider { + height: 1px; + margin: 8px 10px; + padding: 0; + background-color: $dropdown-divider-color; + } + + .separator { width: 100%; height: 1px; margin-top: 8px; @@ -136,6 +144,21 @@ 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; } } @@ -154,6 +177,10 @@ .dropdown-menu-back { display: block; } + + .dropdown-content { + padding: 0 10px; + } } } @@ -167,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; @@ -189,7 +216,7 @@ } .dropdown-select { - width: 300px; + width: $dropdown-width; } .dropdown-menu-align-right { @@ -218,20 +245,11 @@ } } -.dropdown-header { - padding-left: 5px; - padding-right: 5px; - color: $dropdown-header-color; - font-size: 13px; - line-height: 22px; -} .dropdown-title { position: relative; - margin-bottom: 10px; - padding-left: 30px; - padding-right: 30px; - padding-bottom: 10px; + padding: 0 0 15px; + margin: 0 10px 10px; font-weight: 600; line-height: 1; text-align: center; @@ -257,21 +275,26 @@ } .dropdown-menu-close { - right: 0; + right: 7px; + width: 20px; + height: 20px; + top: -1px; } .dropdown-menu-back { - left: 0; + left: 7px; + top: 2px; } .dropdown-input { position: relative; margin-bottom: 10px; + padding: 0 10px; .fa { position: absolute; top: 10px; - right: 10px; + right: 20px; color: #c7c7c7; font-size: 12px; pointer-events: none; @@ -281,6 +304,9 @@ display: none; cursor: pointer; pointer-events: all; + right: 22px; + top: 9px; + font-size: 14px; } &.has-value { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ad0e88cda86..a26ace5cc19 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 { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b05c5df1bd8..9209347f9bc 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -3,7 +3,7 @@ vertical-align: top; } -@media (min-width: $screen-sm-min) { +@media (min-width: $screen-sm-min) { .issues-filters, .issues_bulk_update { .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4cb4129b71b..54cb5461113 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -6,40 +6,6 @@ input { border-radius: $border-radius-base; } -input[type='search'] { - background-color: white; - padding-left: 10px; -} - -input[type='search'].search-input { - background-repeat: no-repeat; - background-position: 10px; - background-size: 16px; - background-position-x: 30%; - padding-left: 10px; - background-color: $gray-light; - - &.search-input[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); - } - - &.search-input::-webkit-input-placeholder { - text-align: center; - } - - &.search-input:-moz-placeholder { /* Firefox 18- */ - text-align: center; - } - - &.search-input::-moz-placeholder { /* Firefox 19+ */ - text-align: center; - } - - &.search-input:-ms-input-placeholder { - text-align: center; - } -} - input[type='text'].danger { background: #f2dede!important; border-color: #d66; @@ -125,7 +91,7 @@ label { } .form-control::-webkit-input-placeholder { - color: #7f8fa4; + color: $gl-placeholder-color; } .input-group { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf881596..fa9038ebaca 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -33,10 +33,15 @@ background: $color; } + .complex-sidebar .nav-primary { + border-right: 1px solid lighten($color, 3%); + } + .sidebar-wrapper { background: $color-darker; .sidebar-user { + border-top: 1px solid lighten($color, 3%); background: $color-darker; color: $color-light; @@ -62,7 +67,6 @@ .count { color: $color-light; - background: $color-dark; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 6a68bb5c115..724980b2208 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,37 +117,17 @@ 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; } } @mixin collapsed-header { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; } .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; @media (min-width: $screen-md-min) { @include collapsed-header; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 5ea4f9a49db..66180f38a4f 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -107,7 +107,7 @@ } .page-title { - .note_created_ago, .new-issue-link { + .note-created-ago, .new-issue-link { display: none; } } @@ -116,7 +116,7 @@ display: none; } - aside:not(.right-sidebar){ + aside:not(.right-sidebar) { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 95bdd6d1ea3..fc3b0a422a7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -100,6 +100,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +111,7 @@ height: 34px; display: inline-block; position: relative; - top: 1px; + top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e82d052f45a..b2fab387e17 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -51,7 +51,7 @@ padding: 10px 15px; } -.select2-drop{ +.select2-drop { color: #7f8fa4; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 9d188317783..1d49249dd80 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,7 +144,7 @@ } a { - padding: 7px 15px; + padding: 7px 12px; font-size: $gl-font-size; line-height: 24px; color: $gray; @@ -169,10 +169,12 @@ } .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + &:before { + content: '('; + } + &:after { + content: ')'; + } } &.back-link i { @@ -191,6 +193,27 @@ } } +.expand-nav a { + color: $gl-icon-color; + width: 60px; + position: fixed; + top: 0; + left: 0; + font-size: 20px; + background: #fff; + height: 59px; + text-align: center; + line-height: 59px; + border-bottom: 1px solid #eee; + transition-duration: .3s; + outline: none; + z-index: 100; + + &:hover { + text-decoration: none; + } +} + .collapse-nav a { width: $sidebar_width; position: fixed; @@ -210,55 +233,12 @@ } .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; - .sidebar-wrapper { - width: $sidebar_collapsed_width; - - .header-logo { - width: $sidebar_collapsed_width; - - a { - padding-left: ($sidebar_collapsed_width - 36) / 2; - - .gitlab-text-container { - display: none; - } - } - } - - .nav-sidebar { - width: $sidebar_collapsed_width; - - li { - width: auto; - - a { - span { - display: none; - } - } - } - } - - .collapse-nav a { - width: $sidebar_collapsed_width; - } - - .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; - - .username { - display: none; - } - } + display: none; } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - @media (min-width: $screen-md-min) { padding-left: $sidebar_width; } @@ -288,6 +268,10 @@ @media (min-width: $screen-sm-min) { padding-right: $sidebar_collapsed_width; } + + .sidebar-collapsed-icon { + cursor: pointer; + } } .right-sidebar-expanded { @@ -300,4 +284,53 @@ @media (min-width: $screen-md-min) { padding-right: $gutter_width; } + + &.with-overlay { + padding-right: $sidebar_collapsed_width; + } +} + +.complex-sidebar { + display: inline-block; + + .nav-primary { + width: 61px; + float: left; + height: 100vh; + + .nav-sidebar { + width: 60px; + + li a { + width: 60px; + + span { + display: none; + } + } + } + } + + .nav-secondary { + $nav-secondary-width: 168px; + + float: left; + width: $nav-secondary-width; + + .nav-sidebar { + width: $nav-secondary-width; + + li { + width: $nav-secondary-width; + + a { + width: $nav-secondary-width; + + i { + display: none; + } + } + } + } + } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index f63ac033234..c72af5dad0a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -56,8 +56,8 @@ $component-active-bg: $brand-info; //## $input-color: $text-color; -$input-border: #e7e9ed; -$input-border-focus: #7f8fa4; +$input-border: $border-color; +$input-border-focus: $focus-border-color; $legend-color: $text-color; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 61e0dd4d672..98fe794d362 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -11,6 +11,7 @@ $gutter_inner_width: 258px; * UI elements */ $border-color: #efeff1; +$focus-border-color: #3aabf0; $table-border-color: #eef0f2; $background-color: #faf9f9; @@ -26,6 +27,7 @@ $gl-text-orange: #d90; $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; +$gl-icon-color: $gl-placeholder-color; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -66,7 +68,7 @@ $header-height: 58px; $fixed-layout-width: 1280px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; -$border-radius-default: 3px; +$border-radius-default: 2px; $btn-transparent-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f; $ssh-key-icon-size: 18px; @@ -166,6 +168,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif /* * Dropdowns */ +$dropdown-border-radius: 2px; +$dropdown-width: 300px; $dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; @@ -176,8 +180,8 @@ $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; -$dropdown-input-focus-border: rgb(58, 171, 240); -$dropdown-input-focus-shadow: rgba(#000, .2); +$dropdown-input-focus-border: $focus-border-color; +$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-toggle-bg: #fff; @@ -193,3 +197,23 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $award-emoji-menu-bg: #fff; $award-emoji-menu-border: #f1f2f4; $award-emoji-new-btn-icon-color: #dcdcdc; + +/* + * Search Box + */ +$search-input-border-color: $dropdown-input-focus-border; +$search-input-focus-shadow-color: $dropdown-input-focus-shadow; +$search-input-width: $dropdown-width; +$location-badge-color: #aaa; +$location-badge-bg: $gray-normal; +$location-icon-color: #e7e9ed; +$location-active-color: $gl-text-color; +$location-active-bg: $search-input-border-color; + +/* + * Notes + */ +$notes-light-color: #8e8e8e; +$notes-action-color: #c3c3c3; +$notes-role-color: #8e8e8e; +$notes-role-border-color: #e4e4e4; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 28994e60baa..37bf38fa65d 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -37,7 +37,7 @@ height: 300px; overflow-y: scroll; - input.emoji-search{ + input.emoji-search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); background-repeat: no-repeat; background-position: right 5px center; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 2a7b5cfc7fd..67a9d7d2cf7 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -42,7 +42,7 @@ } } - .loading{ + .loading { font-size: 20px; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 971656feb42..082911bd118 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; } @@ -74,7 +74,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 5e91496679a..8272615768d 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,4 +1,4 @@ -.commits-compare-switch{ +.commits-compare-switch { @include btn-default; @include btn-white; background: image-url("switch_icon.png") no-repeat center center; @@ -93,7 +93,6 @@ li.commit { .commit-row-info { color: $gl-gray; line-height: 24px; - font-size: 13px; a { color: $gl-gray; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 43be5e38ba8..0f0592a0ab8 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,5 +1,5 @@ .file-editor { - #editor{ + #editor { border: none; @include border-radius(0); height: 500px; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 84eefd01cfe..c66efe978cd 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -43,10 +43,6 @@ .md { color: #7f8fa4; font-size: $gl-font-size; - - iframe.twitter-share-button { - vertical-align: bottom; - } } pre { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5300bb52a1b..88c1b614c74 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/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6d2bd33b28b..6926448519e 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,9 +1,9 @@ .ci-body { - .incorrect-syntax{ + .incorrect-syntax { font-size: 19px; color: red; } - .correct-syntax{ + .correct-syntax { font-size: 19px; color: #47a447; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 777bcbca5c3..403171d4532 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -36,7 +36,7 @@ } } - .login-box{ + .login-box { background: #fafafa; border-radius: 10px; box-shadow: 0 0 2px #ccc; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index cee5c47cfb2..7ff63ca20b6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -230,3 +230,9 @@ } } } + +.builds { + .table-holder { + overflow-x: scroll; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index daf2651425f..655f88b0c2c 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -71,8 +71,6 @@ } .note-form-actions { - background: #fff; - .note-form-option { margin-top: 8px; margin-left: 30px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4bd2016bdcf..92fcaaeeacf 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -22,7 +22,7 @@ ul.notes { margin-left: 55px; } - .note_created_ago, .note-updated-at { + .note-created-ago, .note-updated-at { white-space: nowrap; } @@ -39,53 +39,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; } @@ -198,40 +151,88 @@ ul.notes { border-width: 1px 0; padding-top: 0; vertical-align: top; - &.parallel{ + &.parallel { border-width: 1px; } } } } +.discussion-header, +.note-header { + a { + color: inherit; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } + } + + .author_link { + font-weight: 600; + } +} + +.note-headline-light, +.discussion-headline-light { + color: $notes-light-color; +} + /** * Actions for Discussions/Notes */ -.discussion, -.note { - .discussion-actions, - .note-actions { - float: right; - margin-left: 10px; +.discussion-actions, +.note-actions { + float: right; + margin-left: 10px; + color: $notes-action-color; +} - a { - margin-left: 5px; - color: $gl-gray; +.note-action-button, +.discussion-action-button { + display: inline-block; + margin-left: 10px; + line-height: 24px; - i.fa { - font-size: 16px; - line-height: 16px; - } + .fa { + position: relative; + top: 1px; + font-size: 17px; + } - &:hover { - @extend .cgray; - &.danger { @extend .cred; } - } - } + .fa-trash-o { + top: 0; + font-size: 16px; } } + +.discussion-toggle-button { + line-height: 20px; + font-size: 13px; + + .fa { + margin-right: 3px; + font-size: 10px; + line-height: 18px; + vertical-align: top; + } +} + +.note-role { + position: relative; + top: -2px; + display: inline-block; + padding-left: 4px; + padding-right: 4px; + color: $notes-role-color; + font-size: 12px; + line-height: 20px; + border: 1px solid $notes-role-border-color; + border-radius: $border-radius-base; +} + .diff-file .note .note-actions { right: 0; top: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 71bde1174ee..4e6aa8cd1a6 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,7 +222,7 @@ padding: 0; background: transparent; border: none; - line-height: 42px; + line-height: 36px; margin: 0; > li + li:before { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b6e45024644..3c74d25beb0 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-active-bg; + color: $white-light; + } + + .search-input-wrap { + i { + color: $location-active-color; + } + } + + &.has-location-badge { + .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/todos.scss b/app/assets/stylesheets/pages/todos.scss index f983e9829e6..e83fa9e3d52 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -6,13 +6,19 @@ .navbar-nav { li { .badge.todos-pending-count { - background-color: #7f8fa4; + background-color: $gl-icon-color; margin-top: -5px; font-weight: normal; } } } +.todo { + &:hover { + cursor: pointer; + } +} + .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ed9f6031389..f010436bd36 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :require_two_factor_authentication, :two_factor_grace_period, :gravatar_enabled, - :twitter_sharing_enabled, :sign_in_text, :help_page_text, :home_page_url, diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 081e01a75e0..8bf71a1adbb 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/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 6ff47c4033a..6d4d4360988 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController before_action :no_cache_headers 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/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 877b39c9b1b..6d649e72f84 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 b830d777752..49064f5d505 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html format.json { render json: @merge_request } - format.diff { render text: @merge_request.to_diff(current_user) } - format.patch { render text: @merge_request.to_patch(current_user) } + format.diff { render text: @merge_request.to_diff } + format.patch { render text: @merge_request.to_patch } end end @@ -154,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 @@ -227,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) + ci_commit = @merge_request.ci_commit + if ci_commit + status = ci_commit.status + coverage = ci_commit.try(:coverage) + else + ci_service = @merge_request.source_project.ci_service + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + if ci_service.respond_to?(:commit_coverage) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + end end response = { + title: merge_request.title, + sha: merge_request.last_commit_short_sha, status: status, coverage: coverage } diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b2e974eff17..f7b6d137bde 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/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b578b419a46..6d2901a24a4 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_controller.rb b/app/controllers/projects_controller.rb index 928817ba811..8c3a74c8236 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -71,7 +71,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 +138,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 +235,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/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 046286dd9e1..f1df6832bf6 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 23693629a4c..60a0ff32c9c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,10 +3,6 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 316a10b7da3..14697f774cc 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 diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d3e5e3aa8b9..592bad8ba24 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -214,4 +214,12 @@ module EventsHelper end end end + + def event_row_class(event) + if event.body? + "event-block" + else + "event-inline" + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 81df2094392..b14b8218d02 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/notes_helper.rb b/app/helpers/notes_helper.rb index 53c543c28c5..698f90cb27a 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) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 494dad0b41e..8a97a74ad73 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,4 +1,5 @@ module SearchHelper + def search_autocomplete_opts(term) return unless current_user @@ -23,45 +24,44 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "SSH Keys", url: profile_keys_path }, + { category: "Settings", label: "Dashboard", url: root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api", "README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -72,7 +72,9 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - label: "group: #{search_result_sanitize(group.name)}", + category: "Groups", + id: group.id, + label: "#{search_result_sanitize(group.name)}", url: group_path(group) } end @@ -83,7 +85,10 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", + category: "Projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.name_with_namespace)}", url: namespace_project_path(p.namespace, p) } end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 8cbc9eefc7b..826e5f96fa1 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -110,6 +110,10 @@ class Notify < BaseMailer headers['Reply-To'] = address + fallback_reply_message_id = "".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 fa2345f6faa..c0bf6def7c5 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 c4879598c4e..052cd874733 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null @@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/app/models/commit.rb b/app/models/commit.rb index d0dbe009d0d..d09876a07d9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -74,14 +74,14 @@ class Commit # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?\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) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 289dbc57287..51673897d98 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -43,14 +43,14 @@ class CommitRange # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?#{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 cf5b2c71675..afa2ca039ae 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :todos, as: :target, dependent: :destroy validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -41,7 +42,7 @@ module Issuable scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } - scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) } + scope :non_archived, -> { join_project.where(projects: { archived: false }) } delegate :name, :email, diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 2ca79df0a29..b8585d4e577 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?\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 97bd79af083..da7c265a371 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 ed960cb39f4..e064b0f8b95 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\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 500d5a35521..55c01cae762 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -56,7 +56,7 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ef48207f956..bf185cb5dd8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\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 bbd59eab9ae..986184dd301 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base end def self.link_reference_pattern - super("milestones", /(?\d+)/) + @link_reference_pattern ||= super("milestones", /(?\d+)/) end def self.upcoming @@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) "[#{escaped_title}](#{url})" diff --git a/app/models/note.rb b/app/models/note.rb index b0c33f2eec5..87ced65c650 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -311,7 +311,7 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end - def for_project_snippet? + def for_snippet? noteable_type == "Snippet" end diff --git a/app/models/project.rb b/app/models/project.rb index 941e444a4f8..7b1188420ef 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') } @@ -491,7 +493,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 @@ -612,7 +614,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 @@ -951,16 +953,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/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05436cd0f79..eaa5654b9c6 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,7 +20,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aba37921c09..1ed42c4f3e7 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,7 +21,7 @@ class JiraService < IssueTrackerService include HTTParty - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers DEFAULT_API_VERSION = 2 diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 5af24a80609..438ff33fdff 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 c07e8072043..e80c2238402 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) @@ -335,6 +335,8 @@ class Repository # Runs code just before a repository is deleted. def before_delete + expire_exists_cache + expire_cache if exists? expire_root_ref_cache @@ -612,10 +614,14 @@ class Repository refs_contains_sha('tag', sha) end - def branches - @branches ||= raw_repository.branches + def local_branches + @local_branches ||= rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(branch.name, branch.target) + end end + alias_method :branches, :local_branches + def tags @tags ||= raw_repository.tags end @@ -818,7 +824,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b9e835a4486..b96e3937281 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\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 128ddc2a694..2b0bee2099f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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/issues/move_service.rb b/app/services/issues/move_service.rb index a5efb21fab6..82e7090f1ea 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -43,7 +43,7 @@ module Issues def create_new_issue new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, project: @new_project, author: @old_issue.author, - description: unfold_references(@old_issue.description) } + description: rewrite_content(@old_issue.description) } new_params = @old_issue.serializable_hash.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute @@ -53,7 +53,7 @@ module Issues @old_issue.notes.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, - note: unfold_references(new_note.note), + note: rewrite_content(new_note.note), created_at: note.created_at, updated_at: note.updated_at } @@ -61,6 +61,18 @@ module Issues end end + def rewrite_content(content) + return unless content + + rewriters = [Gitlab::Gfm::ReferenceRewriter, + Gitlab::Gfm::UploadsRewriter] + + rewriters.inject(content) do |text, klass| + rewriter = klass.new(text, @old_project, @current_user) + rewriter.rewrite(@new_project) + end + end + def close_issue close_service = CloseService.new(@old_project, @current_user) close_service.execute(@old_issue, notifications: false, system_note: false) @@ -78,20 +90,12 @@ module Issues direction: :to) end - def unfold_references(content) - return unless content - - rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, - @current_user) - rewriter.rewrite(@new_project) + def mark_as_moved + @old_issue.update(moved_to: @new_issue) end def notify_participants notification_service.issue_moved(@old_issue, @new_issue, @current_user) end - - def mark_as_moved - @old_issue.update(moved_to: @new_issue) - end end end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb new file mode 100644 index 00000000000..315c3e16292 --- /dev/null +++ b/app/services/projects/unlink_fork_service.rb @@ -0,0 +1,19 @@ +module Projects + class UnlinkForkService < BaseService + def execute + return unless @project.forked? + + @project.forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project + end + + merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + + merge_requests.each do |mr| + MergeRequests::CloseService.new(@project, @current_user).execute(mr) + end + + @project.forked_project_link.destroy + end + end +end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index ea2b26ccb52..f0615ec7420 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -95,17 +95,19 @@ class SystemHooksService end def project_member_data(model) + project = model.project || Project.unscoped.find(model.source_id) + { - project_name: model.project.name, - project_path: model.project.path, - project_path_with_namespace: model.project.path_with_namespace, - project_id: model.project.id, - user_username: model.user.username, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + project_name: project.name, + project_path: project.path, + project_path_with_namespace: project.path_with_namespace, + project_id: project.id, + user_username: model.user.username, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e022a046c48..658b086496f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -224,7 +224,7 @@ class SystemNoteService # # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f2662922e90..42c5bca90fd 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -123,7 +123,7 @@ class TodoService def handle_note(note, author) # Skip system notes, and notes on project snippet - return if note.system? || note.for_project_snippet? + return if note.system? || note.for_snippet? project = note.project target = note.noteable @@ -170,14 +170,30 @@ class TodoService end def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users.select do |user| - user.can?(:read_project, project) - end - + mentioned_users = target.mentioned_users + mentioned_users = reject_users_without_access(mentioned_users, project, target) mentioned_users.delete(author) mentioned_users.uniq end + def reject_users_without_access(users, project, target) + if target.is_a?(Note) && target.for_issue? + target = target.noteable + end + + if target.is_a?(Issue) + select_users(users, :read_issue, target) + else + select_users(users, :read_project, project) + end + end + + def select_users(users, ability, subject) + users.select do |user| + user.can?(ability.to_sym, subject) + end + end + def pending_todos(user, criteria = {}) valid_keys = [:project_id, :target_id, :target_type, :commit_id] user.todos.pending.where(criteria.slice(*valid_keys)) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 86d24469e05..1af9e9b0edb 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,14 +1,15 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?[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/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0350995d03d..de86dacbb12 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -76,13 +76,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/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml deleted file mode 100644 index 9c2290bc4a5..00000000000 --- 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 e3a4d64df01..aa0aff86d4d 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,4 +1,4 @@ -%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } +%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' @@ -10,7 +10,10 @@ (removed) %span.todo-label = todo_action_name(todo) - = todo_target_link(todo) + - if todo.target + = todo_target_link(todo) + - else + (removed) · #{time_ago_with_tooltip(todo.created_at)} diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 42c2764e7e2..4d20dd5830e 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ - if event.visible_to_user?(current_user) - .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} + .event-item{ class: event_row_class(event) } .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 8cf36c711b4..5a2a469ba62 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -7,21 +7,3 @@ = link_to_project event.project - else = event.project_name - -- if !event.project.private? && twitter_sharing_enabled? - .event-body{"data-user-is" => event.author_id} - .event-note - .md - %p - Congratulations! Why not share your accomplishment with the world? - - %a.twitter-share-button{ | - href: "https://twitter.com/share", | - "data-url" => event.project.web_url, | - "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | - "data-size" => "medium", | - "data-related" => "gitlab", | - "data-hashtags" => "gitlab", | - "data-count" => "none"} - Tweet - %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml deleted file mode 100644 index 2ed51d87ca1..00000000000 --- a/app/views/layouts/_collapse_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d..9be36273c7d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,7 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" + .expand-nav + = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -8,15 +10,19 @@ .gitlab-text-container %h3 GitLab - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' + - primary_sidebar = current_user ? 'dashboard' : 'explore' + + - if defined?(sidebar) && sidebar && sidebar != primary_sidebar + .complex-sidebar + .nav-primary + = render "layouts/nav/#{primary_sidebar}" + .nav-secondary + = render "layouts/nav/#{sidebar}" - else - = render 'layouts/nav/explore' + = render "layouts/nav/#{primary_sidebar}" .collapse-nav - = render partial: 'layouts/collapse_button' + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 54af2c3063c..9d4ab9847a8 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/ + - label = 'This group' +- if controller.controller_path =~ /^projects/ + - 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/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b93729..22d1d4d8597 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -95,7 +95,7 @@ Spam Logs %span.count= number_with_delimiter(SpamLog.count(:all)) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = nav_link(controller: :application_settings) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4a0069f18f8..d1a180e4299 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,12 +15,12 @@ = icon('dashboard fw') %span Activity - = nav_link(controller: :groups) do + = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups - = nav_link(controller: :milestones) do + = nav_link(path: 'dashboard#milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span @@ -48,7 +48,6 @@ %span Help - %li.separate-item = nav_link(controller: :profile) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 55940741dc0..0b7de9633ec 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -42,7 +34,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do + = nav_link do = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5..cc119fd64e6 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75e..d0f82b5f57f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,19 +1,4 @@ %ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = icon('bookmark fw') @@ -113,7 +98,7 @@ Snippets - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = nav_link(html_options: {class: "#{project_tab_class}"}) do = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index de80abd7f4d..3d15c0d932b 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -56,19 +56,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/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index e7c85edff96..1e4c46fca2f 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -3,25 +3,32 @@ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - if can?(current_user, :create_issue, @project) + - can_create_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - can_create_snippet = can?(current_user, :create_snippet, @project) + + - if can_create_issue %li = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - - if can?(current_user, :create_snippet, @project) + + - if can_create_snippet %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet - - if can?(current_user, :push_code, @project) + - if can_create_issue || merge_project || can_create_snippet %li.divider + + - if can?(current_user, :push_code, @project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') @@ -35,13 +42,11 @@ = icon('tags fw') New tag - elsif current_user && current_user.already_forked?(@project) - %li.divider %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') New file - elsif can?(current_user, :fork_project, @project) - %li.divider %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 88cbb7c03c5..5fb5fe5af2f 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/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 8367112a9cb..2731219ccad 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,7 +1,10 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_commit_id = diff_refs.first.id -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +// diff_refs will be nil for orphaned commits (e.g. first commit in repo) +- if diff_refs + - old_commit_id = diff_refs.first.id + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) + - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 905f6bbbd48..1fe1d98bf13 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/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ee5b9fd95a8..1dd8f721f7e 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/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index b05ab869215..2ec0d20a879 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,15 +1,17 @@ - if @ci_commit .mr-widget-heading - .ci_widget{class: "ci-#{@ci_commit.status}"} - = ci_status_icon(@ci_commit) - %span - Build - = ci_status_label(@ci_commit) - for - = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - %w[success skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -43,5 +45,5 @@ :javascript $(function() { - merge_request_widget.getCiStatus(); + merge_request_widget.getCIStatus(false); }); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b24..2be06aebe6c 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -9,12 +9,17 @@ :javascript var merge_request_widget; - - merge_request_widget = new MergeRequestWidget({ - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + var opts = { + merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", + ci_status: "", + ci_message: "Build {{status}} for \"{{title}}\"", ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}", - }); + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + }; + if(typeof merge_request_widget === 'undefined') { + merge_request_widget = new MergeRequestWidget(opts); + } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 2cf32e6093d..34fe1743f4b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,28 +5,21 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header + = link_to_member(note.project, note.author, avatar: false) + .inline.note-headline-light + = "#{note.author.to_reference} commented" + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - if note_editable?(note) .note-actions - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil-square-o') - - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete 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/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 4f15a99d061..cd8a5f0bd02 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -1,22 +1,20 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + on the diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 3da2f2060b8..46f2ba4bbcf 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 218b0da3977..f8e000b424f 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -1,19 +1,18 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + on the outdated diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on the - %strong outdated diff - %div + .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index ba69569b1e7..1c5f8b3928b 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/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 5fcba2b7e93..9544e3d3e17 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,24 +1,20 @@ - project = note.project +- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on + = link_to project.name_with_namespace, project + · - 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/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f91ff0e3694..921eaefd79a 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' @@ -38,11 +37,10 @@ %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", 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 dropdown-menu-milestone", - placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = 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 178223fb463..e2a9e5bfb92 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -14,7 +14,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 +22,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 diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 006a34a11e3..fd5e58c1f1f 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,4 +1,4 @@ -- if params[:label_name] +- if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 0434506c8d7..2fcf40ece99 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 0e20e86356d..47e544acf52 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -28,6 +28,7 @@ = icon('user') .title.hide-collapsed Assignee + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed @@ -39,10 +40,14 @@ %span.username = issuable.assignee.to_reference - else - .light None + %span.assign-yourself + No assignee - + %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,6 +59,7 @@ No .title.hide-collapsed Milestone + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed @@ -62,10 +68,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,6 +81,7 @@ = issuable.labels.count .title.hide-collapsed Labels + = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } @@ -84,8 +91,31 @@ - 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 +146,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/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 55cb6af232e..ccefd0f71a0 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -5,6 +5,9 @@ class ProjectCacheWorker def perform(project_id) project = Project.find(project_id) + + return unless project.repository.exists? + project.update_repository_size project.update_commit_count diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index d06e4480292..b51c6a266c9 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -5,7 +5,7 @@ class ProjectDestroyWorker def perform(project_id, user_id, params) begin - project = Project.find(project_id) + project = Project.unscoped.find(project_id) rescue ActiveRecord::RecordNotFound return end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 500b745f55e..fb1c3476f65 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -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 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 626268d7648..2b989015279 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -174,7 +174,6 @@ 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? diff --git a/config/mail_room.yml b/config/mail_room.yml index aed55f74eab..60257329f3e 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -17,7 +17,7 @@ if File.exists?(config_file) config['start_tls'] = false if config['start_tls'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil? - if config['enabled'] && config['address'] && config['address'].include?('%{key}') + if config['enabled'] && config['address'] redis_url = Gitlab::RedisConfig.new(rails_env).url %> - diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb index 9fa96203ffd..99289166e81 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 ebb7ae585e6..bd1e016d679 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 1978ea89153..d1174bc3d98 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 b310b35e373..1c758c56ffe 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/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb index 56ce58a846d..56ea97e8561 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/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb new file mode 100644 index 00000000000..1fff9759d1e --- /dev/null +++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedIssues < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'Issue' + AND NOT EXISTS ( + SELECT * + FROM issues + WHERE issues.id = todos.target_id + AND issues.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb new file mode 100644 index 00000000000..275554e736e --- /dev/null +++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb @@ -0,0 +1,6 @@ +class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration + def change + add_index :projects, :pending_delete + end +end + diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb new file mode 100644 index 00000000000..54cea964ff2 --- /dev/null +++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'MergeRequest' + AND NOT EXISTS ( + SELECT * + FROM merge_requests + WHERE merge_requests.id = todos.target_id + AND merge_requests.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb new file mode 100644 index 00000000000..0d736e323b6 --- /dev/null +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -0,0 +1,5 @@ +class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :twitter_sharing_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 75509c35b05..df4c65a0625 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: 20160331133914) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -417,9 +417,9 @@ ActiveRecord::Schema.define(version: 20160320204112) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" - t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -748,6 +748,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 e6fa4fc049b..724c7cca0f1 100644 --- a/doc/README.md +++ b/doc/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. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md new file mode 100644 index 00000000000..07e548aaabe --- /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 00000000000..237700bbcd9 --- /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 18d64c41986..cc6355d34ef 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -237,6 +237,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 diff --git a/doc/api/labels.md b/doc/api/labels.md index 6496ffe9fd1..544e898b6aa 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" }, { "color" : "#d9534f", - "name" : "confirmed" + "name" : "confirmed", + "description": "Confirmed issue" }, { "name" : "critical", - "color" : "#d9534f" + "color" : "#d9534f", + "description": "Criticalissue. Need fix ASAP" }, { "color" : "#428bca", - "name" : "discussion" + "name" : "discussion", + "description": "Issue that needs further discussion" }, { "name" : "documentation", - "color" : "#f0ad4e" + "color" : "#f0ad4e", + "description": "Issue about documentation" }, { "color" : "#5cb85c", - "name" : "enhancement" + "name" : "enhancement", + "description": "Enhancement proposal" }, { "color" : "#428bca", - "name" : "suggestion" + "name" : "suggestion", + "description": "Suggestion" }, { "color" : "#f0ad4e", - "name" : "support" + "name" : "support", + "description": "Support issue" } ] ``` @@ -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/projects.md b/doc/api/projects.md index 3703f4b327a..3a909a2bc87 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,172 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Archive a project + +Archives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus archiving an already archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + +### Unarchive a project + +Unarchives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus unarchiving an non-archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + ### Remove project Removes a project including all associated resources (issues, merge requests etc.) diff --git a/doc/api/settings.md b/doc/api/settings.md index 001de76c7af..1e745115dc8 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -26,7 +26,6 @@ Example response: "default_branch_protection" : 2, "restricted_visibility_levels" : [], "signin_enabled" : true, - "twitter_sharing_enabled" : true, "after_sign_out_path" : null, "max_attachment_size" : 10, "user_oauth_applications" : true, @@ -57,7 +56,6 @@ PUT /application/settings | `sign_in_text` | string | no | Text on login page | | `home_page_url` | string | no | Redirect to this URL when not logged in | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. | -| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter | | `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | @@ -85,7 +83,6 @@ Example response: "updated_at": "2015-06-30T13:22:42.210Z", "home_page_url": "", "default_branch_protection": 2, - "twitter_sharing_enabled": true, "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md index 4cfb8402943..5a9a1582877 100644 --- a/doc/incoming_email/README.md +++ b/doc/incoming_email/README.md @@ -1,36 +1,99 @@ # Reply by email -GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. +GitLab can be set up to allow users to comment on issues and merge requests by +replying to notification emails. -## Get a mailbox +## Requirement -Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. +Reply by email requires an IMAP-enabled email account. GitLab allows you to use +three strategies for this feature: +- using email sub-addressing +- using a dedicated email address +- using a catch-all mailbox -If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). +### Email sub-addressing -To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). +**If your provider or server supports email sub-addressing, we recommend using it.** + +[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is +a feature where any email to `user+some_arbitrary_tag@example.com` will end up +in the mailbox for `user@example.com`, and is supported by providers such as +Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix +mail server which you can run on-premises. + +### Dedicated email address + +This solution is really simple to set up: you just have to create an email +address dedicated to receive your users' replies to GitLab notifications. + +### Catch-all mailbox + +A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will +"catch all" the emails addressed to the domain that do not exist in the mail +server. + +## How it works? + +### 1. GitLab sends a notification email + +When GitLab sends a notification and Reply by email is enabled, the `Reply-To` +header is set to the address defined in your GitLab configuration, with the +`%{key}` placeholder (if present) replaced by a specific "reply key". In +addition, this "reply key" is also added to the `References` header. + +### 2. You reply to the notification email + +When you reply to the notification email, your email client will: + +- send the email to the `Reply-To` address it got from the notification email +- set the `In-Reply-To` header to the value of the `Message-ID` header from the + notification email +- set the `References` header to the value of the `Message-ID` plus the value of + the notification email's `References` header. + +### 3. GitLab receives your reply to the notification email + +When GitLab receives your reply, it will look for the "reply key" in the +following headers, in this order: + +1. the `To` header +1. the `References` header + +If it finds a reply key, it will be able to leave your reply as a comment on +the entity the notification was about (issue, merge request, commit...). + +For more details about the `Message-ID`, `In-Reply-To`, and `References headers`, +please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4). ## Set it up +If you want to use Gmail / Google Apps with Reply by email, make sure you have +[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) +and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255). + +To set up a basic Postfix mail server with IMAP access on Ubuntu, follow +[these instructions](./postfix.md). + ### Omnibus package installations -1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the + feature and fill in the details for your specific IMAP server and email account: ```ruby # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com gitlab_rails['incoming_email_enabled'] = true - - # The email address including a placeholder for the key that references the item being replied to. - # The `%{key}` placeholder is added after the user part, before the `@`. + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" - + # Email account username # With third party providers, this is usually the full email address. # With self-hosted email servers, this is usually the user part of the email address. gitlab_rails['incoming_email_email'] = "incoming" # Email account password gitlab_rails['incoming_email_password'] = "[REDACTED]" - + # IMAP server host gitlab_rails['incoming_email_host'] = "gitlab.example.com" # IMAP server port @@ -47,18 +110,18 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ```ruby # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com gitlab_rails['incoming_email_enabled'] = true - + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" - + # Email account username # With third party providers, this is usually the full email address. # With self-hosted email servers, this is usually the user part of the email address. gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account password gitlab_rails['incoming_email_password'] = "[REDACTED]" - + # IMAP server host gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server port @@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these gitlab_rails['incoming_email_mailbox_name'] = "inbox" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. - 1. Reconfigure GitLab and restart mailroom for the changes to take effect: ```sh @@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these cd /home/git/gitlab ``` -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature + and fill in the details for your specific IMAP server and email account: ```sh sudo editor config/gitlab.yml @@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "incoming+%{key}@gitlab.example.com" # Email account username @@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these enabled: true # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "gitlab-incoming+%{key}@gmail.com" # Email account username @@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these mailbox: "inbox" ``` - As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. - 1. Enable `mail_room` in the init script at `/etc/default/gitlab`: ```sh @@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these incoming_email: enabled: true - # The email address including a placeholder for the key that references the item being replied to. - # The `%{key}` placeholder is added after the user part, before the `@`. + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). address: "gitlab-incoming+%{key}@gmail.com" # Email account username diff --git a/doc/install/installation.md b/doc/install/installation.md index bffbc776500..e0a16df09c1 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 diff --git a/doc/integration/github.md b/doc/integration/github.md index 886784a27c9..1890edd7a4c 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 cf1f98492ea..fb20308c49c 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/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index 712e9fdf93a..b9abcbd2c12 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 00000000000..76eee147c72 --- /dev/null +++ b/doc/update/8.6-to-8.7.md @@ -0,0 +1,146 @@ +# 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 + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index c3b3577c449..db73309804c 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -6,6 +6,7 @@ Feature: Dashboard And project "Shop" has push event And project "Shop" has CI enabled And project "Shop" has CI build + And project "Shop" has labels: "bug", "feature", "enhancement" And I visit dashboard page Scenario: I should see projects list @@ -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 1e7b1b50d64..8677b450813 100644 --- a/features/dashboard/todos.feature +++ b/features/dashboard/todos.feature @@ -36,3 +36,8 @@ Feature: Dashboard Todos Scenario: I filter by action Given I filter by "Mentioned" Then I should not see todos related to "Assignments" in the list + + @javascript + Scenario: I click on a todo row + Given I click on the todo + Then I should be directed to the corresponding page diff --git a/features/groups.feature b/features/groups.feature index 419a5d3963d..49e939807b5 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -7,10 +7,6 @@ Feature: Groups When I visit group "NonExistentGroup" page Then page status code should be 404 - Scenario: I should have back to group button - When I visit group "Owned" page - Then I should see back to dashboard button - @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/project/project.feature b/features/project/project.feature index f1f3ed26065..aa22401c88e 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,15 +18,6 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button - Scenario: I should have back to group button - And project "Shop" belongs to group - And I visit project "Shop" page - Then I should see back to group button - - Scenario: I should have back to group button - And I visit project "Shop" page - Then I should see back to dashboard button - Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 5062e348844..b5980b35102 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see 1 project at group list' do expect(find('span.last_activity/span')).to have_content('1') end + + step 'I filter the list by label "feature"' do + page.within ".labels-filter" do + find('.dropdown').click + click_link "feature" + end + end + + step 'I should see "Bugfix1" in issues list' do + page.within "ul.content-list" do + expect(page).to have_content "Bugfix1" + end + end + + step 'project "Shop" has issue "Bugfix1" with label "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user) + issue.labels << project.labels.find_by(title: 'feature') + end end diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 93aa77589be..e21af72a777 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "All" link' do - find('.js-author-search').click - find('.dropdown-content a', match: :first).click - - find('.js-assignee-search').click - find('.dropdown-content a', match: :first).click + find(".js-author-search").click + find(".dropdown-menu-author li a", match: :first).click + find(".js-assignee-search").click + find(".dropdown-menu-assignee li a", match: :first).click end def should_see(issue) diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 963e4f21365..30b21b93ac7 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -88,6 +88,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/group/milestones.rb b/features/steps/group/milestones.rb index a167d259837..b6ce5bc9cec 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - click_link 'Milestones' + page.within '.nav-secondary' do + click_link("Milestones") + end end step 'I should see group milestones index page has no milestones' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index e5b7db4c5e3..483370f41c6 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedGroup include SharedUser - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 19d81453d8c..4584fc4d754 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -82,7 +82,9 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - click_link('Milestones') + page.within '.nav-secondary' do + click_link('Milestones') + end end step 'I click the "Labels" tab' do diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 527f7853da9..d9b16afa9b8 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I goto the Merge Requests page' do - page.within '.page-sidebar-expanded' do + page.within '.nav-secondary' do click_link "Merge Requests" end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 91fe19dd477..a4f02b590ea 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -334,7 +334,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion by user "John Doe" has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{user_exists("John Doe").name} started a discussion" + page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -350,7 +350,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -358,7 +358,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content "One comment to rule them all" end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index ef185861e00..8f1d4a223a9 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - expect(page).not_to have_link 'Snippets' + page.within '.nav-secondary' do + expect(page).not_to have_link 'Snippets' + end end step 'project "Shop" belongs to group' do @@ -123,14 +125,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps @project.save! end - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - - step 'I should see back to group button' do - expect(page).to have_content 'Go to group' - end - step 'I click notifications drop down button' do click_link 'notifications-button' end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 4fc2ece79ff..fa7d24ce611 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -41,7 +41,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - page.within '.nav-sidebar' do + page.within '.nav-secondary' do expect(page).to have_content('Go to project') end end diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json new file mode 100644 index 00000000000..18d6e93e0f4 --- /dev/null +++ b/fixtures/emojis/digests.json @@ -0,0 +1,8597 @@ +[ + { + "name": "100", + "unicode": "1F4AF", + "digest": "6d57c7cc93335f853e1a5670233f121bc94730dbd82b2b3c5c5a509e092ef0fd" + }, + { + "name": "1234", + "unicode": "1F522", + "digest": "727763fd9f18fd5df59e9f78e678ea4ec753e674d70f15d4e77c7802067d660b" + }, + { + "name": "8ball", + "unicode": "1F3B1", + "digest": "1aecf21951452ba24e921ec71b3d313b7ddc2e185b0339c9e0eebc85be4f031d" + }, + { + "name": "a", + "unicode": "1F170", + "digest": "2272113a5bcb7faf8db7c1bd35df576d32f2f7cbd881463934ad3382eb87c723" + }, + { + "name": "ab", + "unicode": "1F18E", + "digest": "6f8a237751fdc84db4121f408272d9a23258515449610e4c6c54f50f6e995627" + }, + { + "name": "abc", + "unicode": "1F524", + "digest": "652a2381a7b587d8a52d5178e2d7d6c8600b33d36160fa69677943da374105bc" + }, + { + "name": "abcd", + "unicode": "1F521", + "digest": "35ade4fd3d75294ebb72c24490aa32745604edc6cabe095b90634cd3ce78c07b" + }, + { + "name": "accept", + "unicode": "1F251", + "digest": "8212ed158cc447c92813273fc915e84d3d5c4c48d1b38e498c088bad27ab8145" + }, + { + "name": "aerial_tramway", + "unicode": "1F6A1", + "digest": "8039d7f67e6e5b211066cab6cf2142afc3aca5c830a357369362c9b484029563" + }, + { + "name": "airplane", + "unicode": "2708", + "digest": "18f4dfac323555d8cdabb79148874c0185ce98e1a08e69414d236b23e502a854" + }, + { + "name": "airplane_arriving", + "unicode": "1F6EC", + "digest": "9a1c81d97512e5d0e3acec40290d00f616ec182140909859e366a734b9f840bb" + }, + { + "name": "airplane_departure", + "unicode": "1F6EB", + "digest": "e3c5ff4038db998c1897cb237d0b865da0bc60331c758f204e45a979d5fab445" + }, + { + "name": "airplane_northeast", + "unicode": "1F6EA", + "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" + }, + { + "name": "airplane_small", + "unicode": "1F6E9", + "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" + }, + { + "name": "airplane_small_up", + "unicode": "1F6E8", + "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" + }, + { + "name": "airplane_up", + "unicode": "1F6E7", + "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" + }, + { + "name": "alarm_clock", + "unicode": "23F0", + "digest": "84ddd7b3b857c165410b7b44863e5354ca0f3591c3bfe56231f12c9f7531a96f" + }, + { + "name": "alembic", + "unicode": "2697", + "digest": "45698914a21683f06931d807af171bcb6984e5ebce66012bba71b467565bd69d" + }, + { + "name": "alien", + "unicode": "1F47D", + "digest": "94dbe4e90614c654145aba93610c43e3ab86df8ca07391bd4e56383f9329c008" + }, + { + "name": "ambulance", + "unicode": "1F691", + "digest": "82ef36bcd13c88a4b2397c918b8048adc6bf045ed2532ff568e0dfd1b1b29c3c" + }, + { + "name": "amphora", + "unicode": "1F3FA", + "digest": "d3758d88aa1fc3be01894102f57479d3a49790510d38ad3d06a2774962010608" + }, + { + "name": "anchor", + "unicode": "2693", + "digest": "27c6034f769d9f020362fc5b227b9279651cc940861e727d1f6ccd59af98f851" + }, + { + "name": "angel", + "unicode": "1F47C", + "digest": "c1b8ad2adc7686e7fbbe4ec357071e7228a5e0762e001bb589e2f97ff258d5c7" + }, + { + "name": "angel_tone1", + "unicode": "1F47C-1F3FB", + "digest": "90b701c43311b1096c4a012d9905a186f1a16829ea2707921a8418c28617d751" + }, + { + "name": "angel_tone2", + "unicode": "1F47C-1F3FC", + "digest": "d6bcaf1b76e25d486d4ab9b159cf727782d508543d1ae27c8d2c12d2f13d6eb0" + }, + { + "name": "angel_tone3", + "unicode": "1F47C-1F3FD", + "digest": "3069285e6218c8083cb0085aa10017bcdea033e321d97ba339a84892074b903a" + }, + { + "name": "angel_tone4", + "unicode": "1F47C-1F3FE", + "digest": "dbb87019752d9caa94ce086858c1e3225b62e221ad599f5106548fda2456fc2b" + }, + { + "name": "angel_tone5", + "unicode": "1F47C-1F3FF", + "digest": "f77703df97720c27a128b5f3c0948b9e04a6b6b81ea5306468154f9bf56225db" + }, + { + "name": "anger", + "unicode": "1F4A2", + "digest": "2253b7ff0894f247bc6f04d841a748c56d6c94684880c13df42387691ff20e75" + }, + { + "name": "anger_left", + "unicode": "1F5EE", + "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" + }, + { + "name": "anger_right", + "unicode": "1F5EF", + "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" + }, + { + "name": "angry", + "unicode": "1F620", + "digest": "c4188ba70df99d8ccef5706d711176725d3dd50d62f065a177d68d85c7828107" + }, + { + "name": "anguished", + "unicode": "1F627", + "digest": "9c2347308133ae50dc04da62042fff847f4c477b2956b8aa976f0413899e38bc" + }, + { + "name": "ant", + "unicode": "1F41C", + "digest": "d2af2ed1cfe15d649aa329d965764a1e8726941d833841781a5b66d7dd0b0921" + }, + { + "name": "apple", + "unicode": "1F34E", + "digest": "a9babee24f454934a5e1fb8d781cbce354dfd88e8a8e01f02e8b30071fd40460" + }, + { + "name": "aquarius", + "unicode": "2652", + "digest": "1a168c252678847d1f9ef450887489e3bdc207ecae4b6fb05e92295ff861ae2c" + }, + { + "name": "aries", + "unicode": "2648", + "digest": "bde262a8795e12f8b0ebb3f0f8c3a56104062fcee8d5d678cf4bb445a7daf698" + }, + { + "name": "arrow_backward", + "unicode": "25C0", + "digest": "ddae36d1febf5c246e51d599e2898a8aa30cd47f88b5bcb469e3ca9d22538b97" + }, + { + "name": "arrow_double_down", + "unicode": "23EC", + "digest": "906f42b5f788128ed90d2d162cf03e6e595a50ad05e0aa5f64e925637379d0cd" + }, + { + "name": "arrow_double_up", + "unicode": "23EB", + "digest": "2129a57402980de6fc6f59ad8354525c2dbcd66d1b78f4de091181ddc81e0693" + }, + { + "name": "arrow_down", + "unicode": "2B07", + "digest": "370e4f41565d5dab245c20e45c502505a56d26c2392283781b841eb3e905edb2" + }, + { + "name": "arrow_down_small", + "unicode": "1F53D", + "digest": "98a2b183f2daec425160bbfce1d2b940b8baa0d5032fdacfa9453e39bed5651b" + }, + { + "name": "arrow_forward", + "unicode": "25B6", + "digest": "348627b8e0f55cf1e9ab19c9de1d170371b2c4cb4dda9a2aa8e0c558db08b18a" + }, + { + "name": "arrow_heading_down", + "unicode": "2935", + "digest": "96c64953fc3134711247bef320f252c48993ebc90494925b7fee42ffce2a2ec2" + }, + { + "name": "arrow_heading_up", + "unicode": "2934", + "digest": "94f94e74176cc050703b3584f3f700debf86e4e61b893a441825a21fa3f8ce74" + }, + { + "name": "arrow_left", + "unicode": "2B05", + "digest": "4553be62a63d7550deac4f7dbeffce6006f769ae6cddfb8c795671672011ba0b" + }, + { + "name": "arrow_lower_left", + "unicode": "2199", + "digest": "10f83c252110d705cdcfebc35a70c341ad288730d0c0729479e3a96e263d5120" + }, + { + "name": "arrow_lower_right", + "unicode": "2198", + "digest": "ee33abd4c96c19e9b80a2fc1500ba8ecaa6668c49310cc816a496e8c61af3850" + }, + { + "name": "arrow_right", + "unicode": "27A1", + "digest": "2611e9138a2651916f414015d0287f5f0af266514d96a42915d32b04fb652a90" + }, + { + "name": "arrow_right_hook", + "unicode": "21AA", + "digest": "628b06384a2963a4fe81e9fbf4e22511f697878d9b9db7d2fc98f8aadbe8f4f9" + }, + { + "name": "arrow_up", + "unicode": "2B06", + "digest": "c09e5f41c01028b45707c525d30d3d6731ec57b7447f0d7ba4ad6c1404449e5c" + }, + { + "name": "arrow_up_down", + "unicode": "2195", + "digest": "e7fd92d24a01702f76c7fcc0de998bc81fbfb93711d076984f6da91d1dccd84c" + }, + { + "name": "arrow_up_small", + "unicode": "1F53C", + "digest": "bc48dad74bc1d0c5579cbf5e3d005314b0d21bc5b5ebbba2b05136e33f49296d" + }, + { + "name": "arrow_upper_left", + "unicode": "2196", + "digest": "792a9709f03843024e53d201cb4769c59b656c3bf0dff2306e8e605493a66b93" + }, + { + "name": "arrow_upper_right", + "unicode": "2197", + "digest": "ee934b0c9cff270efd30a6cafc15253d405efd2c93b4785ac2ed4ea6420266a6" + }, + { + "name": "arrows_clockwise", + "unicode": "1F503", + "digest": "914f4120513730d7a19c9f8c4e59223a90568de0b25a225b712b31fa9697ef4f" + }, + { + "name": "arrows_counterclockwise", + "unicode": "1F504", + "digest": "86d87597e4e3db6dbba9907ee82412db0cbab1ea875bd0be6505dd886dc19b90" + }, + { + "name": "art", + "unicode": "1F3A8", + "digest": "dfc6b0da780199df86507d65b0499ba1706c266ae7badcb0e7fb5b85af7c9578" + }, + { + "name": "articulated_lorry", + "unicode": "1F69B", + "digest": "4c4de240ebd175f7b53453eda4e51f2e57d0db2a98d317f804116e14e47cff1d" + }, + { + "name": "ascending_notes", + "unicode": "1F39C", + "digest": "33432042771d456338dda5d98e49322d3600f2cc9049963480c7c38d9de1ef0a" + }, + { + "name": "asterisk", + "unicode": "002A-20E3", + "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" + }, + { + "name": "astonished", + "unicode": "1F632", + "digest": "58632b97e274ade5183752db2b3c5c4fe29effcd5a9720a8d01fa809b97023dc" + }, + { + "name": "athletic_shoe", + "unicode": "1F45F", + "digest": "1fc55d85a4d6751f9e60467801b051d2fb3341bdcc33b8d3695d5143359edb43" + }, + { + "name": "atm", + "unicode": "1F3E7", + "digest": "bf827ef6c349f5b6912d821457975a4720d1750529d907e94ece429b7a388d7e" + }, + { + "name": "atom", + "unicode": "269B", + "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" + }, + { + "name": "b", + "unicode": "1F171", + "digest": "9116256b3189977e37f6da7ddedf82bb29b0358829a4e8718fd59e51d9b86b3c" + }, + { + "name": "baby", + "unicode": "1F476", + "digest": "66596bea11015154e0b1752b85f349f4286c6643ee6f51ee5e60e0d625c4ae9a" + }, + { + "name": "baby_bottle", + "unicode": "1F37C", + "digest": "ed42994b4a539b8bfeccde0f3c7e9c7f54d6696ff48ce7e48171bbab51002348" + }, + { + "name": "baby_chick", + "unicode": "1F424", + "digest": "ea2cfa0e5c2cbff5fffdb52cc04dfe7872834bd7cfeaa45e0541b8faffcbd0e9" + }, + { + "name": "baby_symbol", + "unicode": "1F6BC", + "digest": "65df04dff8739b86f7663ae9c0648927341f360a986655e109721b0e16013b75" + }, + { + "name": "baby_tone1", + "unicode": "1F476-1F3FB", + "digest": "bc747527a2d723cf99ef3fc2539c19d29634c92ff417736982d3bf87d65d06eb" + }, + { + "name": "baby_tone2", + "unicode": "1F476-1F3FC", + "digest": "b82bba7a666b7d070751726e54acc7fb8f96e2dfc09e9610d61cfd20947aef9c" + }, + { + "name": "baby_tone3", + "unicode": "1F476-1F3FD", + "digest": "7f45dfd4ea2ae8515d419ffa13e7ee5c625b024b4e521ace5344c414bb929da0" + }, + { + "name": "baby_tone4", + "unicode": "1F476-1F3FE", + "digest": "80b1854626616f15426649cc6415e4911a55c8f761422fe48a08af9e8ac6a7cb" + }, + { + "name": "baby_tone5", + "unicode": "1F476-1F3FF", + "digest": "9f890804d19a61bee76a29644c818045dd96cf69d67cfbca2d11f4ad376b27da" + }, + { + "name": "back", + "unicode": "1F519", + "digest": "1dc73947b8f56e033777ca3f747407923bd16b07e53a6c78b09950ca474b7e7a" + }, + { + "name": "badminton", + "unicode": "1F3F8", + "digest": "3f95180c1175d0248ebf4b8650cf86566c39e0486d828078244080194c14d4fe" + }, + { + "name": "baggage_claim", + "unicode": "1F6C4", + "digest": "7c1a69511aa2a93984d601da4d1cef1cb4cefbbf127b1486278da8c01345bbf3" + }, + { + "name": "balloon", + "unicode": "1F388", + "digest": "a10c2b0865179cdbdef339494ec9b2a109451a356e53738d6a9dd43232500956" + }, + { + "name": "ballot_box", + "unicode": "1F5F3", + "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" + }, + { + "name": "ballot_box_check", + "unicode": "1F5F9", + "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" + }, + { + "name": "ballot_box_with_check", + "unicode": "2611", + "digest": "5f5cec7fe462557d31e8d2b836534c1e76d546cc0061236fa2af3667972b84aa" + }, + { + "name": "ballot_box_x", + "unicode": "1F5F5", + "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" + }, + { + "name": "ballot_x", + "unicode": "1F5F4", + "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" + }, + { + "name": "bamboo", + "unicode": "1F38D", + "digest": "feb0cf2f1012a1c0649b8c66f7e96e2d8bcdefe879c5a52dab3e25c51009e3b2" + }, + { + "name": "banana", + "unicode": "1F34C", + "digest": "aa9a1e6db00efa94a7f414c570eff7fc29011be64031a24d03b7f37b617cfd2d" + }, + { + "name": "bangbang", + "unicode": "203C", + "digest": "bdd350766ccd1c0138f6294f7ebfa3e9867b02bda40a743f7062e52c68358765" + }, + { + "name": "bank", + "unicode": "1F3E6", + "digest": "c9648c93049cf8e7884242e58ae3145383d2e5034c9090e0d34c53f5bbce397f" + }, + { + "name": "bar_chart", + "unicode": "1F4CA", + "digest": "942277f72a5b754b13454dab62c85b1ff3447544f38ec76a285f3be32f6f5d12" + }, + { + "name": "barber", + "unicode": "1F488", + "digest": "e1526eea685aafc56fb83d07f8ff63c9967600e447b0e5f831a17d6153f2062d" + }, + { + "name": "baseball", + "unicode": "26BE", + "digest": "3d028b16a898f3a15874bc9d3891f9fbf59ea1c226c5c774eddb58a712c489ae" + }, + { + "name": "basketball", + "unicode": "1F3C0", + "digest": "b2f5a3904d505db066337a24fc840ef75b49ef4c5f152227d8e632ff82285b12" + }, + { + "name": "basketball_player", + "unicode": "26F9", + "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" + }, + { + "name": "basketball_player_tone1", + "unicode": "26F9-1F3FB", + "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" + }, + { + "name": "basketball_player_tone2", + "unicode": "26F9-1F3FC", + "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" + }, + { + "name": "basketball_player_tone3", + "unicode": "26F9-1F3FD", + "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" + }, + { + "name": "basketball_player_tone4", + "unicode": "26F9-1F3FE", + "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" + }, + { + "name": "basketball_player_tone5", + "unicode": "26F9-1F3FF", + "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" + }, + { + "name": "bath", + "unicode": "1F6C0", + "digest": "ae6301a6354630cd9dc06a5137f23f826d019c8298b2b012b6ff31b773a910b6" + }, + { + "name": "bath_tone1", + "unicode": "1F6C0-1F3FB", + "digest": "fce7ae2e7ef3f7f44f36c2ad49348b4cf7fce0b0c17e1a90a1e85734cee95b2a" + }, + { + "name": "bath_tone2", + "unicode": "1F6C0-1F3FC", + "digest": "4d1c9444f16467488fe939fdad279d6855d28be564e5dcc1990451c4b9ae8c95" + }, + { + "name": "bath_tone3", + "unicode": "1F6C0-1F3FD", + "digest": "9a59a4360effb48af4cbb1a953655ef61e69375407038b4d0bd8068fbaf3cc16" + }, + { + "name": "bath_tone4", + "unicode": "1F6C0-1F3FE", + "digest": "01aafa8a53a08018b9fbf28ec6b3b918d6bd0dee7a891196f32f81f60d114f0e" + }, + { + "name": "bath_tone5", + "unicode": "1F6C0-1F3FF", + "digest": "2733e81ccaee21231c2e47e3310b431e9bd784bf34f0db609f8eadcee359500d" + }, + { + "name": "bathtub", + "unicode": "1F6C1", + "digest": "9515e3bb9ab41350305e64fc6877aae82d51e1ba8ce8b2b4b8ffaeda960820cd" + }, + { + "name": "battery", + "unicode": "1F50B", + "digest": "7d4d475c1d5b1be55c319953e3363ff864fe4fcd921a8aa649b9a547c0894deb" + }, + { + "name": "beach", + "unicode": "1F3D6", + "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" + }, + { + "name": "beach_umbrella", + "unicode": "26F1", + "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" + }, + { + "name": "bear", + "unicode": "1F43B", + "digest": "b5ac126875c20c82b9e3140b143233944a2e4132d781d0b575e83673988523cb" + }, + { + "name": "bed", + "unicode": "1F6CF", + "digest": "1919245d7a76799aad0533eb72db2cbaa1f32ee8231a0c1989d3f233f2d42370" + }, + { + "name": "bee", + "unicode": "1F41D", + "digest": "69ada63403c8dabae39c63ba143143aeb59b66faae6aa82d8342337925a9e6b5" + }, + { + "name": "beer", + "unicode": "1F37A", + "digest": "b71dd6efdb4ce7d9d71fdbf82a2ccf83841fb0cceb119ee7da1e575d3bfa853c" + }, + { + "name": "beers", + "unicode": "1F37B", + "digest": "994108cebfe0c614c05967af4e3864d8adbbfcf7cccef1cbd42a47b7dfabf80c" + }, + { + "name": "beetle", + "unicode": "1F41E", + "digest": "ec351ce238a81711eef00e5be1de2e198423cf524b60e531d435902b44420edc" + }, + { + "name": "beginner", + "unicode": "1F530", + "digest": "13288d9fc221dc02f4181b998104e13c3c5c98d3c4e650186bef59a46d39f6f0" + }, + { + "name": "bell", + "unicode": "1F514", + "digest": "784b9a82814ce14a264e54b3a8f8e706f3c7b763646d9f8174c4aa84ad41ef09" + }, + { + "name": "bellhop", + "unicode": "1F6CE", + "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" + }, + { + "name": "bento", + "unicode": "1F371", + "digest": "d59314b17a8646d4a78fefb7b79f289f33d4aaea893fed4cad0b890df63395e7" + }, + { + "name": "bicyclist", + "unicode": "1F6B4", + "digest": "e7359d615d40325bb08a145cfebde2ecef448deeb21695a34b55d3ccb971447f" + }, + { + "name": "bicyclist_tone1", + "unicode": "1F6B4-1F3FB", + "digest": "e45808faa32f4ffb881d3569c0b8e2c69d4a64665f4d1fae24d7a1e5f1d3ea4b" + }, + { + "name": "bicyclist_tone2", + "unicode": "1F6B4-1F3FC", + "digest": "92a3494270d1da6a117e92402c7898d4a7fffbe3d6143fb9ae445c4827c0c8a4" + }, + { + "name": "bicyclist_tone3", + "unicode": "1F6B4-1F3FD", + "digest": "6fdf1db2bbd08d06b643b08f0f29daeaa20e0b8c8abec21132191f435cc05e42" + }, + { + "name": "bicyclist_tone4", + "unicode": "1F6B4-1F3FE", + "digest": "d9c27848e1bcc8197c858e1ef12a537f4ed6c77fb211b6731388dc88c2bb7a61" + }, + { + "name": "bicyclist_tone5", + "unicode": "1F6B4-1F3FF", + "digest": "4892af1a8a0229a813d7b8e3d88481c2365e3e1a5ce2e0e27ce432c5336da810" + }, + { + "name": "bike", + "unicode": "1F6B2", + "digest": "e726f97b5432f46ed51328c0930d1d63b3a2d7b67c5c2303a5ca997083cfcac1" + }, + { + "name": "bikini", + "unicode": "1F459", + "digest": "7612fcb72c005ae7172260825f588d6995f2bc919cb3d283dd4591f6872a1855" + }, + { + "name": "biohazard", + "unicode": "2623", + "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" + }, + { + "name": "bird", + "unicode": "1F426", + "digest": "3f219e5aa18e2f1febfd368ec133786cd2eab357db79984cb8ba07fed0eec7cd" + }, + { + "name": "birthday", + "unicode": "1F382", + "digest": "9eb1adb0170ab851042cb3da8b64f02f4e4b63e7a07db405b55b50f5bbd3cacf" + }, + { + "name": "black_circle", + "unicode": "26AB", + "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" + }, + { + "name": "black_joker", + "unicode": "1F0CF", + "digest": "1eb85b8e2b93dec221a97a1c309dee3683408f6166e1a1a1bd83cf2f64f007dd" + }, + { + "name": "black_large_square", + "unicode": "2B1B", + "digest": "0ff2112227c38ed8c30b0bddf2300e87d2a244cd7fe81886a1cb1a287a7e8bb6" + }, + { + "name": "black_medium_small_square", + "unicode": "25FE", + "digest": "f1010aa694084ad4655a9d4ce5a1711eaab21029e31bf8798253f0ad644e8abb" + }, + { + "name": "black_medium_square", + "unicode": "25FC", + "digest": "06bf48ffbc84e71bbb90aa0f6c3f9f53533c6fd063ff168cefdb0a050dcf8302" + }, + { + "name": "black_nib", + "unicode": "2712", + "digest": "c1361df4a5ae9f2ed121d26928021e96c6865331861e1960700d39cb1bd49355" + }, + { + "name": "black_small_square", + "unicode": "25AA", + "digest": "d430ec419869fa1b5ba980ddeecb4c5ad5050a2b3421e45048cc184a6fc46899" + }, + { + "name": "black_square_button", + "unicode": "1F532", + "digest": "85b6587b6b2c3544ddb7bc07207b0740e437744ba134835836153899ae396135" + }, + { + "name": "blossom", + "unicode": "1F33C", + "digest": "029bbe385e07e2017dd918d685e107678c9c0e919a3bd1521b7a0d7c9172da05" + }, + { + "name": "blowfish", + "unicode": "1F421", + "digest": "b5ee9f6ffabb74e3024067f016d17a631ee98536cb9c7269d55fa867f95a54fb" + }, + { + "name": "blue_book", + "unicode": "1F4D8", + "digest": "6fbf227fb9facc1957bb9dfb31749cbfe66c3afe8081347f2471fd64ef2e6b3a" + }, + { + "name": "blue_car", + "unicode": "1F699", + "digest": "e61ef2299d11fc01e9d6c496d188a7211633946706f6e771c412368346ca16f4" + }, + { + "name": "blue_heart", + "unicode": "1F499", + "digest": "1af8d04173e0a984360786f6031220000dd548b8c912a68fd51f2ba490a9e16a" + }, + { + "name": "blush", + "unicode": "1F60A", + "digest": "d615cda0f7c185ed8a92008204043ef769f3b7fb5424d595aeaaf3827bcdbd73" + }, + { + "name": "boar", + "unicode": "1F417", + "digest": "c23a06db0337597e361ae581eacd4faf9926c6b7db0510d3599eb2e2a73315cb" + }, + { + "name": "bomb", + "unicode": "1F4A3", + "digest": "0099e7435eba35f4f3ad273993293693a8b5cd110567c95ed83e5b4e2d0978ff" + }, + { + "name": "book", + "unicode": "1F4D6", + "digest": "152408f2ff9949b7cbe57f623e4f875aa8dd0b02317e03cc914e1ea3712b3fc7" + }, + { + "name": "book2", + "unicode": "1F56E", + "digest": "26d6b66a1957e7750b3e22eb2e46d0cc85932977bbb81d3d8482ec1ec58ee12b" + }, + { + "name": "bookmark", + "unicode": "1F516", + "digest": "a2e0c6f5466c1b2fc148b20f6afcf4a878f4df55b0181f61fffa3ff727dcb251" + }, + { + "name": "bookmark_tabs", + "unicode": "1F4D1", + "digest": "16135d62ff440722bd1ce8f84219be6a5eb3120a1597bfda4aeed4a2d9e7d7b2" + }, + { + "name": "books", + "unicode": "1F4DA", + "digest": "ba019e4174639440caec424b30dfa016fe71a6f7436fe63025a2e3609ebfc012" + }, + { + "name": "boom", + "unicode": "1F4A5", + "digest": "ec26246935c99749950612d69c06435ccdc126f14426a48a7599c5b6b91d9d58" + }, + { + "name": "boot", + "unicode": "1F462", + "digest": "7ed639d52e285b0f46064dd4e1f4a8fb5814e1b2dc47c6f93cb349a6ac7ea97a" + }, + { + "name": "bouquet", + "unicode": "1F490", + "digest": "b699f13af218560344f3571436f87b6f8c5c9f0fa0308836937667241b3fc7aa" + }, + { + "name": "bouquet2", + "unicode": "1F395", + "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" + }, + { + "name": "bow", + "unicode": "1F647", + "digest": "5e260c38cfc80cd2f20ef78d982126dbf90934f7afa12c96d0b7b413beb6d4e0" + }, + { + "name": "bow_and_arrow", + "unicode": "1F3F9", + "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" + }, + { + "name": "bow_tone1", + "unicode": "1F647-1F3FB", + "digest": "d3ec7ef70b355ba310d6fae7130a4e4cd11526b6e219474b5678a2b3ba1077f0" + }, + { + "name": "bow_tone2", + "unicode": "1F647-1F3FC", + "digest": "c2905c0feba15fbc533cc6b36038eeda30f729182aa544f1d9164f5ccfed64d5" + }, + { + "name": "bow_tone3", + "unicode": "1F647-1F3FD", + "digest": "298fc646d96c307eaa137c80b403d8355539ed8af13d3954a4ccacef67d341fa" + }, + { + "name": "bow_tone4", + "unicode": "1F647-1F3FE", + "digest": "27db8401aa62a2544b24ff839b332958b5e8c3ab3fd7a289d3c62c654705da60" + }, + { + "name": "bow_tone5", + "unicode": "1F647-1F3FF", + "digest": "168cdf834edb54723cf1c32311d4117c288132c5f76d6c415726c7484158c52a" + }, + { + "name": "bowling", + "unicode": "1F3B3", + "digest": "0e888bcd1a5cc1ea7b07cea255ccb04dcdc87b0337b74cdc96a708aad7975768" + }, + { + "name": "boy", + "unicode": "1F466", + "digest": "f349ab3e1015b4ccda5faab6a355f9c38e36e7c1cd667084563a14a2b11036ea" + }, + { + "name": "boy_tone1", + "unicode": "1F466-1F3FB", + "digest": "4d04a5e45c9f9749de580321a212e14304b4ffcd229fa971fb59d97e6124262f" + }, + { + "name": "boy_tone2", + "unicode": "1F466-1F3FC", + "digest": "0c9d6b6b1b3da68b9ef1f0f01efa4d170a48cfc66de4f577f8669c160b81cc97" + }, + { + "name": "boy_tone3", + "unicode": "1F466-1F3FD", + "digest": "7dbecace78edb2aceffce6cb4d49ca132b93d80c26a8f1526a18832a2f23454a" + }, + { + "name": "boy_tone4", + "unicode": "1F466-1F3FE", + "digest": "49f9c633afa8ff81068c78717e0012f8936fb3dcdb8b57342410f57f0635ae7c" + }, + { + "name": "boy_tone5", + "unicode": "1F466-1F3FF", + "digest": "17e2ec379c7b542e6c2c5deef992af5f1fbaa3e288d1f71c8c984fb91a698cd4" + }, + { + "name": "boys_symbol", + "unicode": "1F6C9", + "digest": "47fadbcb876ca436264ce2f3ebd1472bd68f55cc2b4833bf054335be9dc7a0f2" + }, + { + "name": "bread", + "unicode": "1F35E", + "digest": "43697495538bfed11ed75213af8b1bdc14ef359d9b472cd7f9130fcb0a198680" + }, + { + "name": "bride_with_veil", + "unicode": "1F470", + "digest": "37e75fbb2b0d06c900d51269b99107c60b61453dbf218b54df3011a455cd6dc3" + }, + { + "name": "bride_with_veil_tone1", + "unicode": "1F470-1F3FB", + "digest": "44072e54e0618d2675a5bfd6572108590e51e8e733381e091e8754ee96c2cf20" + }, + { + "name": "bride_with_veil_tone2", + "unicode": "1F470-1F3FC", + "digest": "f0acd961e108db9d9dd5d1b06e708b2eb6a7ef7235d6c8678b9319077faf4fa8" + }, + { + "name": "bride_with_veil_tone3", + "unicode": "1F470-1F3FD", + "digest": "3f7adddb41ead3cd07098799ab2a5b8e8842344307d9045264403fb685f20555" + }, + { + "name": "bride_with_veil_tone4", + "unicode": "1F470-1F3FE", + "digest": "5f7199fd99319651f3a7b3553cc5387c59b65cac1eb020441e19b5c12c807dc7" + }, + { + "name": "bride_with_veil_tone5", + "unicode": "1F470-1F3FF", + "digest": "4b1f6c33dd72a3a11c764bb00e7be7441b39c7af78aae52141276a279d63ab78" + }, + { + "name": "bridge_at_night", + "unicode": "1F309", + "digest": "f81cc36de8edbdf3fe4d55932d5c6c8ad429487ec1f7af044611b6dc950ee09c" + }, + { + "name": "briefcase", + "unicode": "1F4BC", + "digest": "a3c3e802191f3e131683dac1fcd81e294dea72af8e65c94972990924c79c5619" + }, + { + "name": "broken_heart", + "unicode": "1F494", + "digest": "4dee349274c2ea44d1c0395cbd39356b88897b0c45040aa40d8cb2607ee67420" + }, + { + "name": "bug", + "unicode": "1F41B", + "digest": "bac4660ee8dcbef0023691804ee3fad3ea3d4bac20d847a5913cee6e7dca826c" + }, + { + "name": "bulb", + "unicode": "1F4A1", + "digest": "af5394230f95781c7eb8054b1a13732a6e6170318599c79e9ca2a816a5b821a2" + }, + { + "name": "bullettrain_front", + "unicode": "1F685", + "digest": "59afcd289500bd4148b1b91f560a5ce8ac9e1b52eddb8fec857ff5d171f017fb" + }, + { + "name": "bullettrain_side", + "unicode": "1F684", + "digest": "79ff8f579081a2f1c3b05311a18ca432adb026a7860875cea4a5460e49b2a474" + }, + { + "name": "bullhorn", + "unicode": "1F56B", + "digest": "a4ca5cbfe299e8ccd148d17055d2d395cf8515e416bf771044c9a670509a8254" + }, + { + "name": "bullhorn_waves", + "unicode": "1F56C", + "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" + }, + { + "name": "burrito", + "unicode": "1F32F", + "digest": "4babb1af1136ab2334d26495b0be779d0bcc9516fd956fc07ffde427d11122f0" + }, + { + "name": "bus", + "unicode": "1F68C", + "digest": "476e7a5e92f64038e5012205395efead51f1c10b3edb25380f38da97e2412edd" + }, + { + "name": "busstop", + "unicode": "1F68F", + "digest": "3bcf82872ab6abb0278238c71bd004a40c46696bdda05f54c153d45d6fe88f15" + }, + { + "name": "bust_in_silhouette", + "unicode": "1F464", + "digest": "2230844993ab011fe2756a1aa3873ff7d5f7d888bddec408ba0b32e4f6003570" + }, + { + "name": "busts_in_silhouette", + "unicode": "1F465", + "digest": "d1c3cb6d437616834425a53621c0bc0a6b368d745dd9da2300a3db4543d57660" + }, + { + "name": "cactus", + "unicode": "1F335", + "digest": "e87588e6548d201db903dc0523b3ccc83c6b559981d743eae1504ce668cd8be4" + }, + { + "name": "cake", + "unicode": "1F370", + "digest": "3947783d128018f5e396602d0492cb5c31e8e8df98af01eda7cade71aea8d989" + }, + { + "name": "calculator", + "unicode": "1F5A9", + "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" + }, + { + "name": "calendar", + "unicode": "1F4C6", + "digest": "00bb700dd88efbc43bc64263491cdf77965130b1dc23f31e682905c3dfe4040c" + }, + { + "name": "calendar_spiral", + "unicode": "1F5D3", + "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" + }, + { + "name": "calling", + "unicode": "1F4F2", + "digest": "2375828085f2efd17b8a5ebb3cfec1e420190913328a7a0dd9ff0f67c7249ffb" + }, + { + "name": "camel", + "unicode": "1F42B", + "digest": "9ff789ab50b51cd9e7fdc7fbe8d6f913fda95dfd425949f97974548652a53ce1" + }, + { + "name": "camera", + "unicode": "1F4F7", + "digest": "d95192b9ba0f566d8874099125def031e15297d1306989ea9b6a49f7b9b56661" + }, + { + "name": "camera_with_flash", + "unicode": "1F4F8", + "digest": "4db6fb3fdb9a004537dff97f4197c7ed87c9c978ba9ac562ed8bb7c1fa260d38" + }, + { + "name": "camping", + "unicode": "1F3D5", + "digest": "f0855dc78bf6f3d06b3c2fc19180c8ff23d9e22871658fcc26a8fde08d328a0a" + }, + { + "name": "cancellation_x", + "unicode": "1F5D9", + "digest": "cea2f7a48543207615ee06755ded62c2a95a7eaf7d7b68a3fc25e74d94e2c92c" + }, + { + "name": "cancer", + "unicode": "264B", + "digest": "b990f85e9f62017d99526244eaef5c5e56f8808698011e85d44de1d2ed87f1a2" + }, + { + "name": "candle", + "unicode": "1F56F", + "digest": "5eefd555951e65298583009a307acc6fb6d02c88325ef3adf231717e75e5a333" + }, + { + "name": "candy", + "unicode": "1F36C", + "digest": "f14203c408173fbb94b4ee69d6de67226a17dc51b0cbd776f62623ee03fd2eb3" + }, + { + "name": "capital_abcd", + "unicode": "1F520", + "digest": "2a7cc876218b8c244b9802448ee25ce5004671a4f00ea950a636d8c3b766dbef" + }, + { + "name": "capricorn", + "unicode": "2651", + "digest": "03a5fd064c10f47c7fd0ae318c573bb559c269b1b2d61b45aa5b8ce9b5fbd9df" + }, + { + "name": "card_box", + "unicode": "1F5C3", + "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" + }, + { + "name": "card_index", + "unicode": "1F4C7", + "digest": "150950903eccb468981c58b87ed7c1ba44e17f52627d695f660ce96b3d9d6e8e" + }, + { + "name": "carousel_horse", + "unicode": "1F3A0", + "digest": "d6862085550fa139a147dceb1b2b9f950a08dcd01cecd8b8697f9c7992ca054e" + }, + { + "name": "cartridge", + "unicode": "1F5AD", + "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" + }, + { + "name": "cat", + "unicode": "1F431", + "digest": "002208c0c9165971853ee05cd05513175a913376a462a345a939d73401c6acb7" + }, + { + "name": "cat2", + "unicode": "1F408", + "digest": "fbdb726cc035f83784dcfe2d9adb85f8aeec429064aed5c5ca0b8be406068aa5" + }, + { + "name": "cd", + "unicode": "1F4BF", + "digest": "bd4d4eef2cc0b1e4ee1f5280f922743e76f27d35836987801b2b48969eac17d8" + }, + { + "name": "celtic_cross", + "unicode": "1F548", + "digest": "187aac988d7e02085a15f31c4cc0ff25127be5b088e354e65c7b1152bffb40ff" + }, + { + "name": "chains", + "unicode": "26D3", + "digest": "a6a915d9c361e1564e13cf2d33ad5df3d684aa349b8dc5909e6343d67401beb9" + }, + { + "name": "champagne", + "unicode": "1F37E", + "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" + }, + { + "name": "chart", + "unicode": "1F4B9", + "digest": "9fd5f8cd99988bbe0fabc89a0b23e28d1468641d2f9468e82b7148a1948d8236" + }, + { + "name": "chart_with_downwards_trend", + "unicode": "1F4C9", + "digest": "6fe456d76c0a996c12049057b5d60129098a9deddfa2d133cff5c4400e4595a0" + }, + { + "name": "chart_with_upwards_trend", + "unicode": "1F4C8", + "digest": "e83cc4cf4228bd77e030a19755b11cf75cf671f40973c23e240afa54d9de478e" + }, + { + "name": "checkered_flag", + "unicode": "1F3C1", + "digest": "77501c2c66af31f72f5c05f21e87598cd59740b5cfc02926c66dc755bab3c3cf" + }, + { + "name": "cheese", + "unicode": "1F9C0", + "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" + }, + { + "name": "cherries", + "unicode": "1F352", + "digest": "5a0ba73039e4b56e3d16a1c70ad992f41af7a16f6d5ba4b5337bdf338276f0ff" + }, + { + "name": "cherry_blossom", + "unicode": "1F338", + "digest": "b40533225291f539ffe97e4ab1d70d07e179b2f9345b2814355164d0407cf3bf" + }, + { + "name": "chestnut", + "unicode": "1F330", + "digest": "6a2a37899d28326daf36965b343b2646492c2c0cee8871321cc17315d6252a9a" + }, + { + "name": "chicken", + "unicode": "1F414", + "digest": "13d770684a11ea10c0ae7570a98c5dfafd4bfb78ac3f72f46729aef9060b85c0" + }, + { + "name": "children_crossing", + "unicode": "1F6B8", + "digest": "654d2502c1edc57c5ab4237df76db3121f6b8735eb13d30bffd305605a083445" + }, + { + "name": "chipmunk", + "unicode": "1F43F", + "digest": "1ae3c838450afcbbe8a96992481dde252e343ab83546d0789ebed81a78ca9188" + }, + { + "name": "chocolate_bar", + "unicode": "1F36B", + "digest": "2486b7265048eb2294d6be0a0a8a4d6067df95721ace9d131d8f715a27ba8cf0" + }, + { + "name": "christmas_tree", + "unicode": "1F384", + "digest": "454c08870eaa84283c19731ed3b10c4868d2e2f0cc44f2feba0de9ba4cc9c4e1" + }, + { + "name": "church", + "unicode": "26EA", + "digest": "b62e838ffb0dfefeced1707359437b6815e0721783b549212282e08617402f6f" + }, + { + "name": "cinema", + "unicode": "1F3A6", + "digest": "6df56f6a0008d0352740d1e045ffdb702e80c2a6d88b6db1a8bcd27eb3c12dcc" + }, + { + "name": "circus_tent", + "unicode": "1F3AA", + "digest": "f8b7a7f4cf4f9efd20423acc30abb3a28e2a5183b3e39f5cc88e7e0ed7757d64" + }, + { + "name": "city_dusk", + "unicode": "1F306", + "digest": "8779066dc9386d05c951b1df1753983c2937a5f3b84d5fc09ed0b172d4ef914e" + }, + { + "name": "city_sunset", + "unicode": "1F307", + "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" + }, + { + "name": "cityscape", + "unicode": "1F3D9", + "digest": "15251a708d50fc721bd67d8abb2a517c0bade196df3b736e21d79191d749241f" + }, + { + "name": "cl", + "unicode": "1F191", + "digest": "104591d8e7b980cf38dcf8326d36c845384b7a4e6d94c49f36e9946484712a95" + }, + { + "name": "clap", + "unicode": "1F44F", + "digest": "ed6ef8bb78ca1fa295b87222c440c6d5ba4f154f2752bf0d428941260d66aaac" + }, + { + "name": "clap_tone1", + "unicode": "1F44F-1F3FB", + "digest": "57a1fd1fa2578c30b8a47abb84e81af5f5bbc6c301a5daf0c53d4d07b017e777" + }, + { + "name": "clap_tone2", + "unicode": "1F44F-1F3FC", + "digest": "2ad4dcd513e55486f21151bf3792e1febf116574d238545b07b4290901430fdd" + }, + { + "name": "clap_tone3", + "unicode": "1F44F-1F3FD", + "digest": "2d8c705d4fcc162fb65cd51e2c6683f1129ebc72fba13343533f64ede1c62687" + }, + { + "name": "clap_tone4", + "unicode": "1F44F-1F3FE", + "digest": "40ffd41b2b4f59d0040e9d20497e57c4e47f18aeae43fcae02be5c2f50069102" + }, + { + "name": "clap_tone5", + "unicode": "1F44F-1F3FF", + "digest": "be55df1ac7600ba086c2ef6ea223ebc62271fa47876c53ade1a1c0151fdc994c" + }, + { + "name": "clapper", + "unicode": "1F3AC", + "digest": "a8748398f56fd2c1e6e87fe0c77edec444df7c7dd462d43dbcea6d8de97c81c5" + }, + { + "name": "classical_building", + "unicode": "1F3DB", + "digest": "6a607b0666141b51d6e944b04f3f6188a5c026396e6105f1d2a5e6b6350cd66b" + }, + { + "name": "clipboard", + "unicode": "1F4CB", + "digest": "4ca1a0b864a962b111d6bdb65373b779f3fff571ffd32d029666f9b708e1ab73" + }, + { + "name": "clock", + "unicode": "1F570", + "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" + }, + { + "name": "clock1", + "unicode": "1F550", + "digest": "c0550fa0c385920cbdb775bdaaa5e812097a484c4a32e35ebbafe3a364a4a438" + }, + { + "name": "clock10", + "unicode": "1F559", + "digest": "25651ac5520505f326457364428de3679cc22ca57278d4c54cc4b60420fa7b74" + }, + { + "name": "clock1030", + "unicode": "1F565", + "digest": "dbf682bac968fc5a3959af2b96eaaa5ee78306f6341c43c1345b94bc561a3d04" + }, + { + "name": "clock11", + "unicode": "1F55A", + "digest": "333732dd6c3184f257964bcf5a20a6111f9adb04560b5d12dc613636e846df5b" + }, + { + "name": "clock1130", + "unicode": "1F566", + "digest": "005999cb37998adea1645d7df63b2705a42db3b4f1a734891d79af3e833764ff" + }, + { + "name": "clock12", + "unicode": "1F55B", + "digest": "6690e591bec1751e1c5472e0bf52f66779b2113e5b8c6c578e65dbb83d091b16" + }, + { + "name": "clock1230", + "unicode": "1F567", + "digest": "549f3921bcff7f330c5a41e6756d8c15601f1f8278b35b369148771c60be2a6f" + }, + { + "name": "clock130", + "unicode": "1F55C", + "digest": "9332ef07a9dde8ccaa1e58a3e97edee0601a1152fc6d351b782816c838d2a408" + }, + { + "name": "clock2", + "unicode": "1F551", + "digest": "9d1ec8fbdae627880e1c067c10d6a40f1e4494a246c77224b3cd7b287554c4b4" + }, + { + "name": "clock230", + "unicode": "1F55D", + "digest": "3578a39c28695d4e617a648a1eb44e0bb5a8a11dcbe04fa2eb2aea0a60589067" + }, + { + "name": "clock3", + "unicode": "1F552", + "digest": "c2e2a27301b6ac27dc359be590448eb1e65fe87211f1af30a473d8bde4f3db47" + }, + { + "name": "clock330", + "unicode": "1F55E", + "digest": "7a77cf8cf9a98f4767a2dca1d3795be45938eee185db81120d85cedebe128899" + }, + { + "name": "clock4", + "unicode": "1F553", + "digest": "0945c4199400d546350cfff25bc9e9160789d1cf9890b3318bdc462ac6cc9782" + }, + { + "name": "clock430", + "unicode": "1F55F", + "digest": "9fdb6f1fa076c4c6a395dbf6db27499ee447b3558f3aa64d913686c360e428a8" + }, + { + "name": "clock5", + "unicode": "1F554", + "digest": "855b3500eb6d20bb6e51d3a6c9d1a5131c06404c6c149841c7cca52201036428" + }, + { + "name": "clock530", + "unicode": "1F560", + "digest": "a6ebd9f884d45a1f43650351a1f1da9724bc044d7da2f6d99ffb3d1fa0c31c5d" + }, + { + "name": "clock6", + "unicode": "1F555", + "digest": "e38f9fc4f87f12ee602dcf2285d59dbc343fc0fc37662992cfe9866c20f58e87" + }, + { + "name": "clock630", + "unicode": "1F561", + "digest": "735954a650791fc38c845c43998023e652d36e55534850e43952878b8804b2f1" + }, + { + "name": "clock7", + "unicode": "1F556", + "digest": "2c4244ec4019e9624e6ea5a751bb735ab87bead33b1ea160265c81bba3c2f736" + }, + { + "name": "clock730", + "unicode": "1F562", + "digest": "0bcf20e30be1bb23394696770301867e307f8e5014e0ed7d75ed96efe34d625d" + }, + { + "name": "clock8", + "unicode": "1F557", + "digest": "af454047a1765ef1c8355969302a826d4c47f5c61a6ec47fdec3510a8003b0d8" + }, + { + "name": "clock830", + "unicode": "1F563", + "digest": "e48b81dac055dc6d5f7832cf34368329c573d03b35bfe076fed1c6e6d48a82e7" + }, + { + "name": "clock9", + "unicode": "1F558", + "digest": "f2a3d1bc029dc0e6406cdaa96542e77503e4cfb79d99c69cb454b8cf635a73fc" + }, + { + "name": "clock930", + "unicode": "1F564", + "digest": "bb1b2b83052e8e6fb97c48c13bce0d950907e044eb2dabf21d7fed321f75110b" + }, + { + "name": "clockwise_arrows", + "unicode": "1F5D8", + "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" + }, + { + "name": "closed_book", + "unicode": "1F4D5", + "digest": "afd6dae5fa0f59330fc2adb922e92b3410a33a80a2667651718c7dac588010bc" + }, + { + "name": "closed_lock_with_key", + "unicode": "1F510", + "digest": "d0ed5c00f939111ce86f9c741b733b22e04ebbd871aa33da3eb0f46a6f38b707" + }, + { + "name": "closed_umbrella", + "unicode": "1F302", + "digest": "3ef08b299f9170007a5433fe82d0953bf0f75b6685d0ce58972f9af032dc471a" + }, + { + "name": "cloud", + "unicode": "2601", + "digest": "d1e7932551e85c6e86bfb3b41f0c936a6d0953bf9f9119b8cca3eaed22ac0c01" + }, + { + "name": "cloud_lightning", + "unicode": "1F329", + "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" + }, + { + "name": "cloud_rain", + "unicode": "1F327", + "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" + }, + { + "name": "cloud_snow", + "unicode": "1F328", + "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" + }, + { + "name": "cloud_tornado", + "unicode": "1F32A", + "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" + }, + { + "name": "clubs", + "unicode": "2663", + "digest": "5fd19fadd3b0887a6a59819ffbbe33a061055c043200700c31be30e14a5d36d5" + }, + { + "name": "cocktail", + "unicode": "1F378", + "digest": "cf096ebe15b4053702d490cd96f04d565b4993529bcd6d8d50cb821200d1cd92" + }, + { + "name": "coffee", + "unicode": "2615", + "digest": "6ea6128e353d9f74aee99caaaaa30c53f996fb242bf3bffb0fa92e6b4d373e57" + }, + { + "name": "coffin", + "unicode": "26B0", + "digest": "b59772d7aa262c4d7433f9cdf76d50011f4c63421b730c8ab4a08675f730c39f" + }, + { + "name": "cold_sweat", + "unicode": "1F630", + "digest": "f0d0057bf01db8d930f6e4632c5bf8d0b1bc709bcfb6463a1f1973b5f1d70a83" + }, + { + "name": "comet", + "unicode": "2604", + "digest": "00252ec55d1846d95c8d4c704b35251232d9810029fc215a7da08262dd1f3541" + }, + { + "name": "compression", + "unicode": "1F5DC", + "digest": "432fbe66e5e3c38ebfeb4eb03465667a1e1be868b4afe510ec95eadda6481bde" + }, + { + "name": "computer", + "unicode": "1F4BB", + "digest": "99777be010488867c7872b2e235be7c35b1a6f28d92baa921b61ced5491c0257" + }, + { + "name": "computer_old", + "unicode": "1F5B3", + "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" + }, + { + "name": "confetti_ball", + "unicode": "1F38A", + "digest": "e77d0c0970d3d12e123e548639fc0fa3ce41668667e4be55baefc09dfaa22cb0" + }, + { + "name": "confounded", + "unicode": "1F616", + "digest": "0f51db64149151d3d7ae5dce08c9af3d064123524fa36fe1f51a78cbd966b6ea" + }, + { + "name": "confused", + "unicode": "1F615", + "digest": "ed23587432c1be98356156784ca4fe0b374b7b3b371660d45cfb0a1efd44e322" + }, + { + "name": "congratulations", + "unicode": "3297", + "digest": "2a46d640bf24fd4dc7649baf4b28c4adb30eda8d24d70eda07036c85b48195e0" + }, + { + "name": "construction", + "unicode": "1F6A7", + "digest": "73fac9fb5eb91954b0f998f9d05fb953241eed988c134fa42477393159fa34fa" + }, + { + "name": "construction_site", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, + { + "name": "construction_worker", + "unicode": "1F477", + "digest": "2be436fa7ad0a31e328fc6f776044bd1eec35c99541ced891792e3bef738d0a0" + }, + { + "name": "construction_worker_tone1", + "unicode": "1F477-1F3FB", + "digest": "172cebc84f91237a85292c5ab0a105cc3abbb96e7423c4ae81feffd00bdb3b26" + }, + { + "name": "construction_worker_tone2", + "unicode": "1F477-1F3FC", + "digest": "3e9b96ddfd639eefda99ad3a0ad26a28a0f2c8be72988c2bdbd648e6104638b6" + }, + { + "name": "construction_worker_tone3", + "unicode": "1F477-1F3FD", + "digest": "11f83c565168dce5ac2387b873769d85ec4087171d6e92fc766c209ea06cd4f3" + }, + { + "name": "construction_worker_tone4", + "unicode": "1F477-1F3FE", + "digest": "09e320e78e3a2940f0c5a0ef9a235ab72c51e053fd8ff433843fdb62571c8e70" + }, + { + "name": "construction_worker_tone5", + "unicode": "1F477-1F3FF", + "digest": "7ac2a1a0038e7aefea889380be604a98255823587e90799165f7db39dd03a0cc" + }, + { + "name": "control_knobs", + "unicode": "1F39B", + "digest": "9f10e578b410ff6aa7cc7fe806a0f1181893765303c0ca3867b652f1392a8a22" + }, + { + "name": "contruction_site", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, + { + "name": "convenience_store", + "unicode": "1F3EA", + "digest": "1ff4351e4a4503f58ed5d35074a2112c681337e35ffe55332187481685573606" + }, + { + "name": "cookie", + "unicode": "1F36A", + "digest": "5c78ce2e721b0a3767d6ce0b59c1e88fdf94a7edc94e98c4d6b7aadb5b2aeea7" + }, + { + "name": "cool", + "unicode": "1F192", + "digest": "54a96697a5070388ce8364a5ee2e0d78a53acc8b4f6755b1359fd67252cc41e8" + }, + { + "name": "cop", + "unicode": "1F46E", + "digest": "16bee252c2a133bcf57f6d7b8372a61364744a2f662acb90e2005732555135fa" + }, + { + "name": "cop_tone1", + "unicode": "1F46E-1F3FB", + "digest": "2fc52f3ed735e327d12dadb15f9feb7b7f720fc6857b551548a2a84809053817" + }, + { + "name": "cop_tone2", + "unicode": "1F46E-1F3FC", + "digest": "6208f3174ced4f07ba3820ba838b247d7438d69d86eb04927333e7436e56af7e" + }, + { + "name": "cop_tone3", + "unicode": "1F46E-1F3FD", + "digest": "2427d30bdfe127be4d8c3870472cae191eece142c784a5c2809df938f43e7c53" + }, + { + "name": "cop_tone4", + "unicode": "1F46E-1F3FE", + "digest": "6e73f8abdf816f3cb2728b971a5a8d006a236c1d71b2ee1788ab60329f406323" + }, + { + "name": "cop_tone5", + "unicode": "1F46E-1F3FF", + "digest": "4b146465cc95ade7e9ca722e31a1b06311214dae8f7f4d95c6329d56c45b451f" + }, + { + "name": "copyright", + "unicode": "00A9", + "digest": "8143583821085dfc8ac21079fe220288ba3a3b6ca3014dc5dc98b18da77589c1" + }, + { + "name": "corn", + "unicode": "1F33D", + "digest": "0160502226b5f9af81763545f288dbbb20632039d7509f347c751cfdb49dc5b5" + }, + { + "name": "couch", + "unicode": "1F6CB", + "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" + }, + { + "name": "couple", + "unicode": "1F46B", + "digest": "97fe611a613216a1788f9bd88a9deb4714ee123a66b5fd3d0ac916fbb4da7304" + }, + { + "name": "couple_mm", + "unicode": "1F468-2764-1F468", + "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" + }, + { + "name": "couple_with_heart", + "unicode": "1F491", + "digest": "d9701173a5e8dff052ab6a15a42494dbb61dc7146d3734c82916abc9c05f76db" + }, + { + "name": "couple_ww", + "unicode": "1F469-2764-1F469", + "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" + }, + { + "name": "couplekiss", + "unicode": "1F48F", + "digest": "e722730de82397da7c8f88d79319b391e8f01fbe4a9133850cc92ad34e77bd82" + }, + { + "name": "cow", + "unicode": "1F42E", + "digest": "dcc1efef2f02588806a156ed43da959c587d4c576ff6badec77f820ed3ba507f" + }, + { + "name": "cow2", + "unicode": "1F404", + "digest": "dcf59f92fd0a37b2ca720bcda606defa4357b58d8f4ad15c1288ad8d814b2bc7" + }, + { + "name": "crab", + "unicode": "1F980", + "digest": "59d34a4e92326ebeab188d9e33b25c20f4d54d187c274713fa3256b03b9e662a" + }, + { + "name": "crayon", + "unicode": "1F58D", + "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" + }, + { + "name": "credit_card", + "unicode": "1F4B3", + "digest": "708c0e7008e06e5d1b3b4e68a7e0ada9f4ae22ab6c28285d81a340f913fd9a84" + }, + { + "name": "crescent_moon", + "unicode": "1F319", + "digest": "0959f838a410e8bfeebf00aa9658df56e515dbd2361142021071e17244662bfc" + }, + { + "name": "cricket", + "unicode": "1F3CF", + "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" + }, + { + "name": "crocodile", + "unicode": "1F40A", + "digest": "99abcb42264d40d2450aaca8c3759a019bfd600a311cf3027243f1ca200d4639" + }, + { + "name": "cross", + "unicode": "271D", + "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" + }, + { + "name": "cross_heavy", + "unicode": "1F547", + "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" + }, + { + "name": "cross_white", + "unicode": "1F546", + "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" + }, + { + "name": "crossbones", + "unicode": "1F571", + "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" + }, + { + "name": "crossed_flags", + "unicode": "1F38C", + "digest": "d4da057db289bec83f0106a94c89bd0cd9b52c7c7f8bc69bc8cbce480d53e12b" + }, + { + "name": "crossed_swords", + "unicode": "2694", + "digest": "f159978583fa77c73ba6de85d35c4195cbd55963e537bd2bfd8f98ab8ff3559a" + }, + { + "name": "crown", + "unicode": "1F451", + "digest": "e6fe2a28b7d80749ca121cabbe89321dcecdd760a122e73fb1562ea9bb40e90d" + }, + { + "name": "cruise_ship", + "unicode": "1F6F3", + "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" + }, + { + "name": "cry", + "unicode": "1F622", + "digest": "2d6a096796222c29b050f74db6b5aff9b9f61390c5eb56e45d1801918751002f" + }, + { + "name": "crying_cat_face", + "unicode": "1F63F", + "digest": "df057d4e3e5c5c87caedf87ea3a6f936811b93f228f46bb7018d2bb5afaa6d35" + }, + { + "name": "crystal_ball", + "unicode": "1F52E", + "digest": "7de438f88134c32c4db67d705e5fecf2a6187a87f56ebbb5bcc5ba09626e2935" + }, + { + "name": "cupid", + "unicode": "1F498", + "digest": "7cb3f7d1ddf9678982197ef0e65735fb465ae8e3652d611f37d3bcccf4d7e2c1" + }, + { + "name": "curly_loop", + "unicode": "27B0", + "digest": "881a43ae406cb74b2ef136bf970db9928bcdc3bbbb7393e90d2c597fe1dd9a96" + }, + { + "name": "currency_exchange", + "unicode": "1F4B1", + "digest": "c4d76e9e61fac8d3c0cb9e07f1fbf1a7fcac6f4d4c78776ff7f04fc9391ce689" + }, + { + "name": "curry", + "unicode": "1F35B", + "digest": "ebe41ee864c873e3a371888c0087b11dbcb124335812895002ed81fe2b6ba571" + }, + { + "name": "custard", + "unicode": "1F36E", + "digest": "afc192f405c30e2d529ec0f4b31a7faf474bcd01fded5294dc38880b8bb22155" + }, + { + "name": "customs", + "unicode": "1F6C3", + "digest": "5abb98151a79cebc1032c0ea149617093e42f41e50574a790a91074cabaa4c3a" + }, + { + "name": "cyclone", + "unicode": "1F300", + "digest": "ae77e15bf2f312f03dbc5c7813d304005bbb549953482db9beb91810c585dc0e" + }, + { + "name": "dagger", + "unicode": "1F5E1", + "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" + }, + { + "name": "dancer", + "unicode": "1F483", + "digest": "e050db55afbb968e02219a58c7e82b824848d299a4df64f0d08d4e1872816203" + }, + { + "name": "dancer_tone1", + "unicode": "1F483-1F3FB", + "digest": "350f6b2e4589fdd436173163035621b8da0bd49c7b9ec9f39593aae5e0ed0641" + }, + { + "name": "dancer_tone2", + "unicode": "1F483-1F3FC", + "digest": "a9efc84ec80582f286147ca34162a27fd5989f4030084acdbc309d4368660f5b" + }, + { + "name": "dancer_tone3", + "unicode": "1F483-1F3FD", + "digest": "ef187f44278fdb8605c80f5cf199e0b3de8a49085dada2e215bb91e1d7d3be5d" + }, + { + "name": "dancer_tone4", + "unicode": "1F483-1F3FE", + "digest": "5195bc352dc9d24cc5505a167c756038e55c05048c61799ea1bfdf2debe44ac2" + }, + { + "name": "dancer_tone5", + "unicode": "1F483-1F3FF", + "digest": "55cb7eee9fa11a16a3932800a19e334546f7396df6aadde22e58fe3185926b16" + }, + { + "name": "dancers", + "unicode": "1F46F", + "digest": "39e7dfd9dafeee20f2968960b1179ee4bf3f2b63a3035fc1944024d0ae8b5de1" + }, + { + "name": "dango", + "unicode": "1F361", + "digest": "2a1b50abe5dc72335344878d9b701028ccad651964d9e3affeedbf3c2bfd652a" + }, + { + "name": "dark_sunglasses", + "unicode": "1F576", + "digest": "6bb1e911a93d5eb0581d3ce8f8929125d3d8fc04e086f3263cfd25af1348ce6c" + }, + { + "name": "dart", + "unicode": "1F3AF", + "digest": "6f28741543a4c1eead21856128ffea1fcf772954fe6af40844dfde47f092ed32" + }, + { + "name": "dash", + "unicode": "1F4A8", + "digest": "25aef37611f1c2f2e96518bf8aeba80580dca9634c8505d390c147388adf6746" + }, + { + "name": "date", + "unicode": "1F4C5", + "digest": "de591b8fad608be761b839beefe9e4c2316320bcf0c44c543a1bc4b89923d938" + }, + { + "name": "deciduous_tree", + "unicode": "1F333", + "digest": "ff31a52096ac1eae770f7f71b6d802198add2c8b4d9d7c9327071b6d6ab86c7b" + }, + { + "name": "department_store", + "unicode": "1F3EC", + "digest": "c1e200d5fdd792121acabdb17bbcfe8e28a63757cfd895c72d4909f14de95ac2" + }, + { + "name": "descending_notes", + "unicode": "1F39D", + "digest": "f09c6a2e094b13bf91cc07b7b776e43348ccef9f91247ca36cc02e7d91098af0" + }, + { + "name": "desert", + "unicode": "1F3DC", + "digest": "e45815250bfc5411de516f87efa218874bcda4b0420b4c17182efc22ba0ce80d" + }, + { + "name": "desktop", + "unicode": "1F5A5", + "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" + }, + { + "name": "desktop_window", + "unicode": "1F5D4", + "digest": "d5b6c4a847e2a96f97f50fd353a22cb121915cb1d7bbc0f02df38769819b6b7e" + }, + { + "name": "diamond_shape_with_a_dot_inside", + "unicode": "1F4A0", + "digest": "4e0e6364b8682dec9a9e20676161c9c9c0faf0a5fdd5402ca2668b18f2bb850a" + }, + { + "name": "diamonds", + "unicode": "2666", + "digest": "42b13b2ed8e5fc63fbe81263c06cc203ba18a45ed5cc2a4fdbf617d219a0d3b4" + }, + { + "name": "disappointed", + "unicode": "1F61E", + "digest": "7f1a619fef84960a9f312d17a58aa58105a4f20a4072efb10227892ab22475d8" + }, + { + "name": "disappointed_relieved", + "unicode": "1F625", + "digest": "a389f5e0a4b619dbc406217967fb1f8f3d0e49b3f790e554ae0ececadbf98967" + }, + { + "name": "dividers", + "unicode": "1F5C2", + "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" + }, + { + "name": "dizzy", + "unicode": "1F4AB", + "digest": "d6fba9b906f0eabd46686e416273a2ca6634249374385f2abf7ed284f0eef995" + }, + { + "name": "dizzy_face", + "unicode": "1F635", + "digest": "b55e20c1551a2912bb5ec64a66c788c9d6f21594cc1da66032188f3814b03f40" + }, + { + "name": "do_not_litter", + "unicode": "1F6AF", + "digest": "126f8c4085e0a8de8241f211f96c3f42c3e3400ea7d8fdf79a14443c3eceb972" + }, + { + "name": "document", + "unicode": "1F5CE", + "digest": "2cbca96cc69306a10f1a9b6505723e027239439d899f6b395dc43f3c37d2d777" + }, + { + "name": "document_text", + "unicode": "1F5B9", + "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" + }, + { + "name": "dog", + "unicode": "1F436", + "digest": "c7b729de8a0967b1f38c3fa5ded94e77e329588caeaaf43abfd1090f420e62bf" + }, + { + "name": "dog2", + "unicode": "1F415", + "digest": "e1897ca60bb3d2662cbe7933352e2b9c50739adf5901d3328797bf399575b97a" + }, + { + "name": "dollar", + "unicode": "1F4B5", + "digest": "7db1e57f799439df1295d42b5249393f1e8cacc8df54caf30499c967a7282742" + }, + { + "name": "dolls", + "unicode": "1F38E", + "digest": "398e7ff5780328700aadded7ce8c50757b1096af5cec66cc4d813a6714686b6d" + }, + { + "name": "dolphin", + "unicode": "1F42C", + "digest": "27385af08848d93acdd13f72751074c2cbccb5ab3c6047e334598af74ed4862d" + }, + { + "name": "door", + "unicode": "1F6AA", + "digest": "3365d7834086328ecbf1da0037f1cf1d0eb49534e173f7962a9e8f4b2ab87e26" + }, + { + "name": "doughnut", + "unicode": "1F369", + "digest": "b4b99fdfe8d07b49cbdd78f8c57e4424819a4ffc8a3ba4867da44cbb3b3a5cca" + }, + { + "name": "dove", + "unicode": "1F54A", + "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" + }, + { + "name": "dragon", + "unicode": "1F409", + "digest": "d7d016568b54d67017681a075fb799d4a2a790ecfa2946d02dbcee629eb4975d" + }, + { + "name": "dragon_face", + "unicode": "1F432", + "digest": "4d0025f1df63b62448477a8f08a50704e15caafb10fea476b529113f41797ab9" + }, + { + "name": "dress", + "unicode": "1F457", + "digest": "02d56ed227280eaf5ad92830ee304afb81f74bb5a13c855397bcd04dd7fa51fb" + }, + { + "name": "dromedary_camel", + "unicode": "1F42A", + "digest": "5afe8a0b73f9f4560264020b1e02a566149dbc38c15a00d2fb5cd90b32d09a75" + }, + { + "name": "droplet", + "unicode": "1F4A7", + "digest": "a92c419792cbd3ba90ed21547362134cfac3e17a5304ee4e3872c9f7b561f834" + }, + { + "name": "dvd", + "unicode": "1F4C0", + "digest": "1ba23e2f01ced5e192e4c1d2f766d9bce400470e81c81410139fd3c0739422df" + }, + { + "name": "e-mail", + "unicode": "1F4E7", + "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" + }, + { + "name": "ear", + "unicode": "1F442", + "digest": "70ba1103a34e68590d91a3b6f8acdbad3b1c65e46e31e26ee1cb855c1e21095e" + }, + { + "name": "ear_of_rice", + "unicode": "1F33E", + "digest": "ddd5f3cc83dbdafd9115861eecd0128e52165bb1dd0049df06ffc564b650d384" + }, + { + "name": "ear_tone1", + "unicode": "1F442-1F3FB", + "digest": "72977be94f5d287a09d175f98fba8b7955ae13aa12ce8e029c0ca875c02ee820" + }, + { + "name": "ear_tone2", + "unicode": "1F442-1F3FC", + "digest": "5ff2e46cb3be7f13b8b94092246b58dab4c2a9ee2a5a46e0b84cf35a6928141f" + }, + { + "name": "ear_tone3", + "unicode": "1F442-1F3FD", + "digest": "19b523f5ada2acaea94b922059c458a3303f4da1dd4c197cf25d31a0e6ecc4b2" + }, + { + "name": "ear_tone4", + "unicode": "1F442-1F3FE", + "digest": "6a5cca9f49c539ef7d0883a2f39652f33ee2d3b25dca0234e4ba027ebbb2b466" + }, + { + "name": "ear_tone5", + "unicode": "1F442-1F3FF", + "digest": "a0a56e8abd36e9be6e2448bcee6f56ecb8bf62d728b19ab6e8f9c6338e226b67" + }, + { + "name": "earth_africa", + "unicode": "1F30D", + "digest": "d4921b543d7cf0c7344fa50c5e4d5a76c208d900be852adc1ee82ed4e8861a39" + }, + { + "name": "earth_americas", + "unicode": "1F30E", + "digest": "61691e6aa9b8d90fc7f75fbc6cc7add5c36022d38f3e05c9d7c54dc44cf865bb" + }, + { + "name": "earth_asia", + "unicode": "1F30F", + "digest": "262904cb552c7f5cf828a11071b3d430a74824b7464e8759ef93ee23b1705767" + }, + { + "name": "egg", + "unicode": "1F373", + "digest": "a7dd617cad489c481ffd14937d9ed491cdd5756903e00473f42600c2fbefb600" + }, + { + "name": "eggplant", + "unicode": "1F346", + "digest": "e5402e8ae5b7f9699ed86b97c242f7939d5731c5a364a2d5b9d04ea5d293cda1" + }, + { + "name": "eight", + "unicode": "0038-20E3", + "digest": "34e293d3228e4643a0132d592f96db91b651fe6ced056ac3c8a3fd49c5ed3416" + }, + { + "name": "eight_pointed_black_star", + "unicode": "2734", + "digest": "c3c2da75731a9a0f4f0a8d1f9cffef75c35e19b7f5d4081da33ac12b46be5fc2" + }, + { + "name": "eight_spoked_asterisk", + "unicode": "2733", + "digest": "cc69618c1074d2b00e6f2c49df5e2c5ff6f4c0fae305505eb8c9daa65a0ea340" + }, + { + "name": "electric_plug", + "unicode": "1F50C", + "digest": "732e1d1675233a0b4643cb73d0c352f8a5a56a11ee90d26627ad1e43c2e4a8e5" + }, + { + "name": "elephant", + "unicode": "1F418", + "digest": "08df3910c4d5d8f49a72c47dd938195e495bde8fd8b3e7b17098a2c1afc41634" + }, + { + "name": "end", + "unicode": "1F51A", + "digest": "05844ab9dcb43deff86f04617af6ea09215595de1415dcfaae018bced57938fe" + }, + { + "name": "envelope", + "unicode": "2709", + "digest": "aad272511d0db910437ba25cf1fb9c806d47aad92a232edb87055916daf4676a" + }, + { + "name": "envelope_back", + "unicode": "1F582", + "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" + }, + { + "name": "envelope_flying", + "unicode": "1F585", + "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" + }, + { + "name": "envelope_stamped", + "unicode": "1F583", + "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" + }, + { + "name": "envelope_stamped_pen", + "unicode": "1F586", + "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" + }, + { + "name": "envelope_with_arrow", + "unicode": "1F4E9", + "digest": "c1ba19b5e7cf64c547ac46eee139e6af70700d49ab511a96e6828c30feb116bc" + }, + { + "name": "euro", + "unicode": "1F4B6", + "digest": "f571952583ffecfa5777065e4d1b680c423d25bc80e567a48fb5d7a1c1b5e735" + }, + { + "name": "european_castle", + "unicode": "1F3F0", + "digest": "db82e383975d079a7bb006e7868035088d75c33bd4031cf8466b71089b65426f" + }, + { + "name": "european_post_office", + "unicode": "1F3E4", + "digest": "d9b38e0f0ca3ad8895b40c767bdbb2b142ccaf03a86c2f275f57a31ed478801a" + }, + { + "name": "evergreen_tree", + "unicode": "1F332", + "digest": "60d8b2d86b20255341f7ecad6d0f178ba9db5fa6b3de92f1b439cdb19f2fc0b1" + }, + { + "name": "exclamation", + "unicode": "2757", + "digest": "cd900ecf82de2b26f0d7783dac4b3232ae94d2cddad5bfacea2eaf65b7ac0a09" + }, + { + "name": "expressionless", + "unicode": "1F611", + "digest": "2ec9466b2d629907ce4c3e24e57f7ee556d2258ff011d972e14d0ae969a40c51" + }, + { + "name": "eye", + "unicode": "1F441", + "digest": "790841e8fce647173eec3c5019440ad9c7e916c535f92acb3132bd92df148cad" + }, + { + "name": "eye_in_speech_bubble", + "unicode": "1F441-1F5E8", + "digest": "bcde5a89a7653bff302685d9d632dd2723796a7ac73125fb7b9493d1ca848e0a" + }, + { + "name": "eyeglasses", + "unicode": "1F453", + "digest": "fd140bef19c420bafe59368d35dd58a58a53e7145b104bae94be10f90679213b" + }, + { + "name": "eyes", + "unicode": "1F440", + "digest": "57ed1f87ebe2485ea32ea69abdb8c5f7ccdcc149b33e74230d801f0883c68c5d" + }, + { + "name": "factory", + "unicode": "1F3ED", + "digest": "6e6b35ae013e5dd26852c9a95d05c39e89c1c1950a33f47e7b951c34af18f37c" + }, + { + "name": "fallen_leaf", + "unicode": "1F342", + "digest": "28ba8628065ffa973b525dd1455691c828d49c2b8c814af387880c13f6707f7e" + }, + { + "name": "family", + "unicode": "1F46A", + "digest": "b5307f86e54cfea581e8406f4b95c801e250a893a9d208cc9a69a6d910b90932" + }, + { + "name": "family_mmb", + "unicode": "1F468-1F468-1F466", + "digest": "49a753c3fcd4420800dd1cda585dae6bfa81615ad4862b477246456f86dc9e82" + }, + { + "name": "family_mmbb", + "unicode": "1F468-1F468-1F466-1F466", + "digest": "882a3a0048efd666b0ab3a07b9f08041aa3a2acdab02664d0feff30bbfa70d68" + }, + { + "name": "family_mmg", + "unicode": "1F468-1F468-1F467", + "digest": "45dd75c19d260a658c8ac93cf878976b96d2000f0efc9c59e72dacc80afb08fa" + }, + { + "name": "family_mmgb", + "unicode": "1F468-1F468-1F467-1F466", + "digest": "910f44a348a951d36ee1f1484d237085bec5083c3875a4d908831dfc64530eaf" + }, + { + "name": "family_mmgg", + "unicode": "1F468-1F468-1F467-1F467", + "digest": "012e75ad0d1b16c2ce63bf80a1ebfb1fc194229cfaf1241039599b82832f6aee" + }, + { + "name": "family_mwbb", + "unicode": "1F468-1F469-1F466-1F466", + "digest": "049a32f61c54f093d2124e25f8b2ec7eac13161e2f2ebf6dc067797698cbe831" + }, + { + "name": "family_mwg", + "unicode": "1F468-1F469-1F467", + "digest": "ba32c637caba634bda99ccba2a1a2a4b6f33aaaed933c30c7d5a51e8de1790d0" + }, + { + "name": "family_mwgb", + "unicode": "1F468-1F469-1F467-1F466", + "digest": "198faba987f45429329b93bbce4f111329f284558bf0eecfa1424186b5f009fe" + }, + { + "name": "family_mwgg", + "unicode": "1F468-1F469-1F467-1F467", + "digest": "3fa2e57cba314dcff04cf8186914823e1e081aabf34fa7437b05c58015df400c" + }, + { + "name": "family_wwb", + "unicode": "1F469-1F469-1F466", + "digest": "b9592fc110a25a478569075deaa520308ef74579cd47aa44df9836599d68143f" + }, + { + "name": "family_wwbb", + "unicode": "1F469-1F469-1F466-1F466", + "digest": "88f398997835fcf5153f17f6baf0deeb2a9c25ce2f8422192c18ac23e90b3193" + }, + { + "name": "family_wwg", + "unicode": "1F469-1F469-1F467", + "digest": "c8d859d3c957fe0d535efccde295fe99bab76e3d28ab5a49c8e736608461cb2e" + }, + { + "name": "family_wwgb", + "unicode": "1F469-1F469-1F467-1F466", + "digest": "006506e4a3d0c82642a0c8481ce95e5e3b969e20fe2def0a16dd686afddbc705" + }, + { + "name": "family_wwgg", + "unicode": "1F469-1F469-1F467-1F467", + "digest": "2553f0deab133aad09b99411d9dd68b56fede30f55ee1f354358767765e36673" + }, + { + "name": "fast_forward", + "unicode": "23E9", + "digest": "1baaed10969b60c083da754ee056bb71df36182cc65af40640acfb76f6b39200" + }, + { + "name": "fax", + "unicode": "1F4E0", + "digest": "b0a392192d03bd5d1ad5ee8eea933cf64725b1776819537bbed27561d78192e7" + }, + { + "name": "fearful", + "unicode": "1F628", + "digest": "7c4cc4de3357c2a6d6e779342b09dabb3ef832a32f2778a0ba074b446f588e8f" + }, + { + "name": "feet", + "unicode": "1F43E", + "digest": "cae13fb54ec64dbcf86ea25bebe2b79877e2d4f5d810b867f095f1d3dfc7f144" + }, + { + "name": "ferris_wheel", + "unicode": "1F3A1", + "digest": "a710a8a0fb039d953313b75330db37e3228d856593547b1f04dc83c00168b987" + }, + { + "name": "ferry", + "unicode": "26F4", + "digest": "21ea239b5adb68dc1ce6c5a1993b0a0b835ef6cc7a0a27cb890838d8475504f6" + }, + { + "name": "field_hockey", + "unicode": "1F3D1", + "digest": "1e46c7f0b5b79c90a5d211ea14cd7e358b1a26a3c8294439253f2b08d0e5c92e" + }, + { + "name": "file_cabinet", + "unicode": "1F5C4", + "digest": "c0b7bdab6c98909eb0fbf1ac89da0008bb00ddb1cb57fe64b4a5ac993eeb18c9" + }, + { + "name": "file_folder", + "unicode": "1F4C1", + "digest": "d98f93c6d7283df0c45f08d3d31ecf5b91b6db1b735959f19e42bfada500a0d1" + }, + { + "name": "film_frames", + "unicode": "1F39E", + "digest": "754a0a60e978f8299a0c4f8959e1f9260f01683e15ae943db430036f01a79b18" + }, + { + "name": "finger_pointing_down", + "unicode": "1F597", + "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" + }, + { + "name": "finger_pointing_down2", + "unicode": "1F59F", + "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" + }, + { + "name": "finger_pointing_left", + "unicode": "1F598", + "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" + }, + { + "name": "finger_pointing_right", + "unicode": "1F599", + "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" + }, + { + "name": "finger_pointing_up", + "unicode": "1F59E", + "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" + }, + { + "name": "fire", + "unicode": "1F525", + "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" + }, + { + "name": "fire_engine", + "unicode": "1F692", + "digest": "3ae03fa34a7088ada95458eb4ee3e97691b3489149f6bbc168086f0483ed3bb2" + }, + { + "name": "fire_engine_oncoming", + "unicode": "1F6F1", + "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" + }, + { + "name": "fireworks", + "unicode": "1F386", + "digest": "3dee83a27c406960253ca1460eb88a599c7b81506051b69605a421b17fe8282c" + }, + { + "name": "first_quarter_moon", + "unicode": "1F313", + "digest": "8fa066362d77bd889090bbe0904ca47f34704e29781c67133c6eaa521c3e1972" + }, + { + "name": "first_quarter_moon_with_face", + "unicode": "1F31B", + "digest": "8877edb366f8eaa00fd83200acf5a17c3b84d246a250519d565dda3aea866ec3" + }, + { + "name": "fish", + "unicode": "1F41F", + "digest": "9ce742108794cc15e59f7719623ae938efbd8155c93ad72585a32f4e32ea9414" + }, + { + "name": "fish_cake", + "unicode": "1F365", + "digest": "1b5b14509287e30da9b8d7abcec376b247f9095aea4bf3fc320349f061a4c321" + }, + { + "name": "fishing_pole_and_fish", + "unicode": "1F3A3", + "digest": "35db56776db1fcec7c8479922d57d54da2577cfe44a894bfd78c51c950c450fb" + }, + { + "name": "fist", + "unicode": "270A", + "digest": "6b80ac2e4d8b830ae06f7c1626d456460094e4ba20c20fb82dabb6b3d2ce7605" + }, + { + "name": "fist_tone1", + "unicode": "270A-1F3FB", + "digest": "d7c79f4f988dd68f064baa5a3a568ab299f8d409db45c8463f39b80e5dd6081f" + }, + { + "name": "fist_tone2", + "unicode": "270A-1F3FC", + "digest": "d1108194e2d962f9ccd00131876d769a8e003117a460d18b2ccbf93e0a0ea346" + }, + { + "name": "fist_tone3", + "unicode": "270A-1F3FD", + "digest": "12f5644b632c95a5c2e41cc9af299e286e266db8b3860091ef5be5f0c4ccc026" + }, + { + "name": "fist_tone4", + "unicode": "270A-1F3FE", + "digest": "521a3ac573381f3bc37a08ddd2d122767aaa0b6b7a38050d3671a12343351816" + }, + { + "name": "fist_tone5", + "unicode": "270A-1F3FF", + "digest": "604e5a234da1b9160e506b3c9026faf9e04268fced7b44baa1ef5e3d4efa83a4" + }, + { + "name": "five", + "unicode": "0035-20E3", + "digest": "0cbd6cd11eb6c2d67749112750d125f4f0a07b53bb7bfb1de0986d943ea9d632" + }, + { + "name": "flag_ac", + "unicode": "1F1E6-1F1E8", + "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" + }, + { + "name": "flag_ad", + "unicode": "1F1E6-1F1E9", + "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" + }, + { + "name": "flag_ae", + "unicode": "1F1E6-1F1EA", + "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" + }, + { + "name": "flag_af", + "unicode": "1F1E6-1F1EB", + "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" + }, + { + "name": "flag_ag", + "unicode": "1F1E6-1F1EC", + "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" + }, + { + "name": "flag_ai", + "unicode": "1F1E6-1F1EE", + "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" + }, + { + "name": "flag_al", + "unicode": "1F1E6-1F1F1", + "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" + }, + { + "name": "flag_am", + "unicode": "1F1E6-1F1F2", + "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" + }, + { + "name": "flag_ao", + "unicode": "1F1E6-1F1F4", + "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" + }, + { + "name": "flag_aq", + "unicode": "1F1E6-1F1F6", + "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" + }, + { + "name": "flag_ar", + "unicode": "1F1E6-1F1F7", + "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" + }, + { + "name": "flag_as", + "unicode": "1F1E6-1F1F8", + "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" + }, + { + "name": "flag_at", + "unicode": "1F1E6-1F1F9", + "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" + }, + { + "name": "flag_au", + "unicode": "1F1E6-1F1FA", + "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" + }, + { + "name": "flag_aw", + "unicode": "1F1E6-1F1FC", + "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" + }, + { + "name": "flag_ax", + "unicode": "1F1E6-1F1FD", + "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" + }, + { + "name": "flag_az", + "unicode": "1F1E6-1F1FF", + "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" + }, + { + "name": "flag_ba", + "unicode": "1F1E7-1F1E6", + "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" + }, + { + "name": "flag_bb", + "unicode": "1F1E7-1F1E7", + "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" + }, + { + "name": "flag_bd", + "unicode": "1F1E7-1F1E9", + "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" + }, + { + "name": "flag_be", + "unicode": "1F1E7-1F1EA", + "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" + }, + { + "name": "flag_bf", + "unicode": "1F1E7-1F1EB", + "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" + }, + { + "name": "flag_bg", + "unicode": "1F1E7-1F1EC", + "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" + }, + { + "name": "flag_bh", + "unicode": "1F1E7-1F1ED", + "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" + }, + { + "name": "flag_bi", + "unicode": "1F1E7-1F1EE", + "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" + }, + { + "name": "flag_bj", + "unicode": "1F1E7-1F1EF", + "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" + }, + { + "name": "flag_bl", + "unicode": "1F1E7-1F1F1", + "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" + }, + { + "name": "flag_black", + "unicode": "1F3F4", + "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" + }, + { + "name": "flag_bm", + "unicode": "1F1E7-1F1F2", + "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" + }, + { + "name": "flag_bn", + "unicode": "1F1E7-1F1F3", + "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" + }, + { + "name": "flag_bo", + "unicode": "1F1E7-1F1F4", + "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" + }, + { + "name": "flag_bq", + "unicode": "1F1E7-1F1F6", + "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" + }, + { + "name": "flag_br", + "unicode": "1F1E7-1F1F7", + "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" + }, + { + "name": "flag_bs", + "unicode": "1F1E7-1F1F8", + "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" + }, + { + "name": "flag_bt", + "unicode": "1F1E7-1F1F9", + "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" + }, + { + "name": "flag_bv", + "unicode": "1F1E7-1F1FB", + "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" + }, + { + "name": "flag_bw", + "unicode": "1F1E7-1F1FC", + "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" + }, + { + "name": "flag_by", + "unicode": "1F1E7-1F1FE", + "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" + }, + { + "name": "flag_bz", + "unicode": "1F1E7-1F1FF", + "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" + }, + { + "name": "flag_ca", + "unicode": "1F1E8-1F1E6", + "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" + }, + { + "name": "flag_cc", + "unicode": "1F1E8-1F1E8", + "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" + }, + { + "name": "flag_cd", + "unicode": "1F1E8-1F1E9", + "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" + }, + { + "name": "flag_cf", + "unicode": "1F1E8-1F1EB", + "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" + }, + { + "name": "flag_cg", + "unicode": "1F1E8-1F1EC", + "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" + }, + { + "name": "flag_ch", + "unicode": "1F1E8-1F1ED", + "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" + }, + { + "name": "flag_ci", + "unicode": "1F1E8-1F1EE", + "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" + }, + { + "name": "flag_ck", + "unicode": "1F1E8-1F1F0", + "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" + }, + { + "name": "flag_cl", + "unicode": "1F1E8-1F1F1", + "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" + }, + { + "name": "flag_cm", + "unicode": "1F1E8-1F1F2", + "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" + }, + { + "name": "flag_cn", + "unicode": "1F1E8-1F1F3", + "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" + }, + { + "name": "flag_co", + "unicode": "1F1E8-1F1F4", + "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" + }, + { + "name": "flag_cp", + "unicode": "1F1E8-1F1F5", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_cr", + "unicode": "1F1E8-1F1F7", + "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" + }, + { + "name": "flag_cu", + "unicode": "1F1E8-1F1FA", + "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" + }, + { + "name": "flag_cv", + "unicode": "1F1E8-1F1FB", + "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" + }, + { + "name": "flag_cw", + "unicode": "1F1E8-1F1FC", + "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" + }, + { + "name": "flag_cx", + "unicode": "1F1E8-1F1FD", + "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" + }, + { + "name": "flag_cy", + "unicode": "1F1E8-1F1FE", + "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" + }, + { + "name": "flag_cz", + "unicode": "1F1E8-1F1FF", + "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" + }, + { + "name": "flag_de", + "unicode": "1F1E9-1F1EA", + "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" + }, + { + "name": "flag_dg", + "unicode": "1F1E9-1F1EC", + "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" + }, + { + "name": "flag_dj", + "unicode": "1F1E9-1F1EF", + "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" + }, + { + "name": "flag_dk", + "unicode": "1F1E9-1F1F0", + "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" + }, + { + "name": "flag_dm", + "unicode": "1F1E9-1F1F2", + "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" + }, + { + "name": "flag_do", + "unicode": "1F1E9-1F1F4", + "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" + }, + { + "name": "flag_dz", + "unicode": "1F1E9-1F1FF", + "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" + }, + { + "name": "flag_ea", + "unicode": "1F1EA-1F1E6", + "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" + }, + { + "name": "flag_ec", + "unicode": "1F1EA-1F1E8", + "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" + }, + { + "name": "flag_ee", + "unicode": "1F1EA-1F1EA", + "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" + }, + { + "name": "flag_eg", + "unicode": "1F1EA-1F1EC", + "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" + }, + { + "name": "flag_eh", + "unicode": "1F1EA-1F1ED", + "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" + }, + { + "name": "flag_er", + "unicode": "1F1EA-1F1F7", + "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" + }, + { + "name": "flag_es", + "unicode": "1F1EA-1F1F8", + "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" + }, + { + "name": "flag_et", + "unicode": "1F1EA-1F1F9", + "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" + }, + { + "name": "flag_eu", + "unicode": "1F1EA-1F1FA", + "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" + }, + { + "name": "flag_fi", + "unicode": "1F1EB-1F1EE", + "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" + }, + { + "name": "flag_fj", + "unicode": "1F1EB-1F1EF", + "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" + }, + { + "name": "flag_fk", + "unicode": "1F1EB-1F1F0", + "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" + }, + { + "name": "flag_fm", + "unicode": "1F1EB-1F1F2", + "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" + }, + { + "name": "flag_fo", + "unicode": "1F1EB-1F1F4", + "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" + }, + { + "name": "flag_fr", + "unicode": "1F1EB-1F1F7", + "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" + }, + { + "name": "flag_ga", + "unicode": "1F1EC-1F1E6", + "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" + }, + { + "name": "flag_gb", + "unicode": "1F1EC-1F1E7", + "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" + }, + { + "name": "flag_gd", + "unicode": "1F1EC-1F1E9", + "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" + }, + { + "name": "flag_ge", + "unicode": "1F1EC-1F1EA", + "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" + }, + { + "name": "flag_gf", + "unicode": "1F1EC-1F1EB", + "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" + }, + { + "name": "flag_gg", + "unicode": "1F1EC-1F1EC", + "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" + }, + { + "name": "flag_gh", + "unicode": "1F1EC-1F1ED", + "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" + }, + { + "name": "flag_gi", + "unicode": "1F1EC-1F1EE", + "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" + }, + { + "name": "flag_gl", + "unicode": "1F1EC-1F1F1", + "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" + }, + { + "name": "flag_gm", + "unicode": "1F1EC-1F1F2", + "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" + }, + { + "name": "flag_gn", + "unicode": "1F1EC-1F1F3", + "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" + }, + { + "name": "flag_gp", + "unicode": "1F1EC-1F1F5", + "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" + }, + { + "name": "flag_gq", + "unicode": "1F1EC-1F1F6", + "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" + }, + { + "name": "flag_gr", + "unicode": "1F1EC-1F1F7", + "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" + }, + { + "name": "flag_gs", + "unicode": "1F1EC-1F1F8", + "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" + }, + { + "name": "flag_gt", + "unicode": "1F1EC-1F1F9", + "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" + }, + { + "name": "flag_gu", + "unicode": "1F1EC-1F1FA", + "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" + }, + { + "name": "flag_gw", + "unicode": "1F1EC-1F1FC", + "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" + }, + { + "name": "flag_gy", + "unicode": "1F1EC-1F1FE", + "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" + }, + { + "name": "flag_hk", + "unicode": "1F1ED-1F1F0", + "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" + }, + { + "name": "flag_hm", + "unicode": "1F1ED-1F1F2", + "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" + }, + { + "name": "flag_hn", + "unicode": "1F1ED-1F1F3", + "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" + }, + { + "name": "flag_hr", + "unicode": "1F1ED-1F1F7", + "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" + }, + { + "name": "flag_ht", + "unicode": "1F1ED-1F1F9", + "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" + }, + { + "name": "flag_hu", + "unicode": "1F1ED-1F1FA", + "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" + }, + { + "name": "flag_ic", + "unicode": "1F1EE-1F1E8", + "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" + }, + { + "name": "flag_id", + "unicode": "1F1EE-1F1E9", + "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" + }, + { + "name": "flag_ie", + "unicode": "1F1EE-1F1EA", + "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" + }, + { + "name": "flag_il", + "unicode": "1F1EE-1F1F1", + "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" + }, + { + "name": "flag_im", + "unicode": "1F1EE-1F1F2", + "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" + }, + { + "name": "flag_in", + "unicode": "1F1EE-1F1F3", + "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" + }, + { + "name": "flag_io", + "unicode": "1F1EE-1F1F4", + "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" + }, + { + "name": "flag_iq", + "unicode": "1F1EE-1F1F6", + "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" + }, + { + "name": "flag_ir", + "unicode": "1F1EE-1F1F7", + "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" + }, + { + "name": "flag_is", + "unicode": "1F1EE-1F1F8", + "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" + }, + { + "name": "flag_it", + "unicode": "1F1EE-1F1F9", + "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" + }, + { + "name": "flag_je", + "unicode": "1F1EF-1F1EA", + "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" + }, + { + "name": "flag_jm", + "unicode": "1F1EF-1F1F2", + "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" + }, + { + "name": "flag_jo", + "unicode": "1F1EF-1F1F4", + "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" + }, + { + "name": "flag_jp", + "unicode": "1F1EF-1F1F5", + "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" + }, + { + "name": "flag_ke", + "unicode": "1F1F0-1F1EA", + "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" + }, + { + "name": "flag_kg", + "unicode": "1F1F0-1F1EC", + "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" + }, + { + "name": "flag_kh", + "unicode": "1F1F0-1F1ED", + "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" + }, + { + "name": "flag_ki", + "unicode": "1F1F0-1F1EE", + "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" + }, + { + "name": "flag_km", + "unicode": "1F1F0-1F1F2", + "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" + }, + { + "name": "flag_kn", + "unicode": "1F1F0-1F1F3", + "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" + }, + { + "name": "flag_kp", + "unicode": "1F1F0-1F1F5", + "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" + }, + { + "name": "flag_kr", + "unicode": "1F1F0-1F1F7", + "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" + }, + { + "name": "flag_kw", + "unicode": "1F1F0-1F1FC", + "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" + }, + { + "name": "flag_ky", + "unicode": "1F1F0-1F1FE", + "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" + }, + { + "name": "flag_kz", + "unicode": "1F1F0-1F1FF", + "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" + }, + { + "name": "flag_la", + "unicode": "1F1F1-1F1E6", + "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" + }, + { + "name": "flag_lb", + "unicode": "1F1F1-1F1E7", + "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" + }, + { + "name": "flag_lc", + "unicode": "1F1F1-1F1E8", + "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" + }, + { + "name": "flag_li", + "unicode": "1F1F1-1F1EE", + "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" + }, + { + "name": "flag_lk", + "unicode": "1F1F1-1F1F0", + "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" + }, + { + "name": "flag_lr", + "unicode": "1F1F1-1F1F7", + "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" + }, + { + "name": "flag_ls", + "unicode": "1F1F1-1F1F8", + "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" + }, + { + "name": "flag_lt", + "unicode": "1F1F1-1F1F9", + "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" + }, + { + "name": "flag_lu", + "unicode": "1F1F1-1F1FA", + "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" + }, + { + "name": "flag_lv", + "unicode": "1F1F1-1F1FB", + "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" + }, + { + "name": "flag_ly", + "unicode": "1F1F1-1F1FE", + "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" + }, + { + "name": "flag_ma", + "unicode": "1F1F2-1F1E6", + "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" + }, + { + "name": "flag_mc", + "unicode": "1F1F2-1F1E8", + "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" + }, + { + "name": "flag_md", + "unicode": "1F1F2-1F1E9", + "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" + }, + { + "name": "flag_me", + "unicode": "1F1F2-1F1EA", + "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" + }, + { + "name": "flag_mf", + "unicode": "1F1F2-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_mg", + "unicode": "1F1F2-1F1EC", + "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" + }, + { + "name": "flag_mh", + "unicode": "1F1F2-1F1ED", + "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" + }, + { + "name": "flag_mk", + "unicode": "1F1F2-1F1F0", + "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" + }, + { + "name": "flag_ml", + "unicode": "1F1F2-1F1F1", + "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" + }, + { + "name": "flag_mm", + "unicode": "1F1F2-1F1F2", + "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" + }, + { + "name": "flag_mn", + "unicode": "1F1F2-1F1F3", + "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" + }, + { + "name": "flag_mo", + "unicode": "1F1F2-1F1F4", + "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" + }, + { + "name": "flag_mp", + "unicode": "1F1F2-1F1F5", + "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" + }, + { + "name": "flag_mq", + "unicode": "1F1F2-1F1F6", + "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" + }, + { + "name": "flag_mr", + "unicode": "1F1F2-1F1F7", + "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" + }, + { + "name": "flag_ms", + "unicode": "1F1F2-1F1F8", + "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" + }, + { + "name": "flag_mt", + "unicode": "1F1F2-1F1F9", + "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" + }, + { + "name": "flag_mu", + "unicode": "1F1F2-1F1FA", + "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" + }, + { + "name": "flag_mv", + "unicode": "1F1F2-1F1FB", + "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" + }, + { + "name": "flag_mw", + "unicode": "1F1F2-1F1FC", + "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" + }, + { + "name": "flag_mx", + "unicode": "1F1F2-1F1FD", + "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" + }, + { + "name": "flag_my", + "unicode": "1F1F2-1F1FE", + "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" + }, + { + "name": "flag_mz", + "unicode": "1F1F2-1F1FF", + "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" + }, + { + "name": "flag_na", + "unicode": "1F1F3-1F1E6", + "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" + }, + { + "name": "flag_nc", + "unicode": "1F1F3-1F1E8", + "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" + }, + { + "name": "flag_ne", + "unicode": "1F1F3-1F1EA", + "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" + }, + { + "name": "flag_nf", + "unicode": "1F1F3-1F1EB", + "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" + }, + { + "name": "flag_ng", + "unicode": "1F1F3-1F1EC", + "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" + }, + { + "name": "flag_ni", + "unicode": "1F1F3-1F1EE", + "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" + }, + { + "name": "flag_nl", + "unicode": "1F1F3-1F1F1", + "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" + }, + { + "name": "flag_no", + "unicode": "1F1F3-1F1F4", + "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" + }, + { + "name": "flag_np", + "unicode": "1F1F3-1F1F5", + "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" + }, + { + "name": "flag_nr", + "unicode": "1F1F3-1F1F7", + "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" + }, + { + "name": "flag_nu", + "unicode": "1F1F3-1F1FA", + "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" + }, + { + "name": "flag_nz", + "unicode": "1F1F3-1F1FF", + "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" + }, + { + "name": "flag_om", + "unicode": "1F1F4-1F1F2", + "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" + }, + { + "name": "flag_pa", + "unicode": "1F1F5-1F1E6", + "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" + }, + { + "name": "flag_pe", + "unicode": "1F1F5-1F1EA", + "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" + }, + { + "name": "flag_pf", + "unicode": "1F1F5-1F1EB", + "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" + }, + { + "name": "flag_pg", + "unicode": "1F1F5-1F1EC", + "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" + }, + { + "name": "flag_ph", + "unicode": "1F1F5-1F1ED", + "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" + }, + { + "name": "flag_pk", + "unicode": "1F1F5-1F1F0", + "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" + }, + { + "name": "flag_pl", + "unicode": "1F1F5-1F1F1", + "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" + }, + { + "name": "flag_pm", + "unicode": "1F1F5-1F1F2", + "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" + }, + { + "name": "flag_pn", + "unicode": "1F1F5-1F1F3", + "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" + }, + { + "name": "flag_pr", + "unicode": "1F1F5-1F1F7", + "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" + }, + { + "name": "flag_ps", + "unicode": "1F1F5-1F1F8", + "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" + }, + { + "name": "flag_pt", + "unicode": "1F1F5-1F1F9", + "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" + }, + { + "name": "flag_pw", + "unicode": "1F1F5-1F1FC", + "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" + }, + { + "name": "flag_py", + "unicode": "1F1F5-1F1FE", + "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" + }, + { + "name": "flag_qa", + "unicode": "1F1F6-1F1E6", + "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" + }, + { + "name": "flag_re", + "unicode": "1F1F7-1F1EA", + "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" + }, + { + "name": "flag_ro", + "unicode": "1F1F7-1F1F4", + "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" + }, + { + "name": "flag_rs", + "unicode": "1F1F7-1F1F8", + "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" + }, + { + "name": "flag_ru", + "unicode": "1F1F7-1F1FA", + "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" + }, + { + "name": "flag_rw", + "unicode": "1F1F7-1F1FC", + "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" + }, + { + "name": "flag_sa", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, + { + "name": "flag_sb", + "unicode": "1F1F8-1F1E7", + "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" + }, + { + "name": "flag_sc", + "unicode": "1F1F8-1F1E8", + "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" + }, + { + "name": "flag_sd", + "unicode": "1F1F8-1F1E9", + "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" + }, + { + "name": "flag_se", + "unicode": "1F1F8-1F1EA", + "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" + }, + { + "name": "flag_sg", + "unicode": "1F1F8-1F1EC", + "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" + }, + { + "name": "flag_sh", + "unicode": "1F1F8-1F1ED", + "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" + }, + { + "name": "flag_si", + "unicode": "1F1F8-1F1EE", + "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" + }, + { + "name": "flag_sj", + "unicode": "1F1F8-1F1EF", + "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" + }, + { + "name": "flag_sk", + "unicode": "1F1F8-1F1F0", + "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" + }, + { + "name": "flag_sl", + "unicode": "1F1F8-1F1F1", + "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" + }, + { + "name": "flag_sm", + "unicode": "1F1F8-1F1F2", + "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" + }, + { + "name": "flag_sn", + "unicode": "1F1F8-1F1F3", + "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" + }, + { + "name": "flag_so", + "unicode": "1F1F8-1F1F4", + "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" + }, + { + "name": "flag_sr", + "unicode": "1F1F8-1F1F7", + "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" + }, + { + "name": "flag_ss", + "unicode": "1F1F8-1F1F8", + "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" + }, + { + "name": "flag_st", + "unicode": "1F1F8-1F1F9", + "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" + }, + { + "name": "flag_sv", + "unicode": "1F1F8-1F1FB", + "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" + }, + { + "name": "flag_sx", + "unicode": "1F1F8-1F1FD", + "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" + }, + { + "name": "flag_sy", + "unicode": "1F1F8-1F1FE", + "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" + }, + { + "name": "flag_sz", + "unicode": "1F1F8-1F1FF", + "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" + }, + { + "name": "flag_ta", + "unicode": "1F1F9-1F1E6", + "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" + }, + { + "name": "flag_tc", + "unicode": "1F1F9-1F1E8", + "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" + }, + { + "name": "flag_td", + "unicode": "1F1F9-1F1E9", + "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" + }, + { + "name": "flag_tf", + "unicode": "1F1F9-1F1EB", + "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" + }, + { + "name": "flag_tg", + "unicode": "1F1F9-1F1EC", + "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" + }, + { + "name": "flag_th", + "unicode": "1F1F9-1F1ED", + "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" + }, + { + "name": "flag_tj", + "unicode": "1F1F9-1F1EF", + "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" + }, + { + "name": "flag_tk", + "unicode": "1F1F9-1F1F0", + "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" + }, + { + "name": "flag_tl", + "unicode": "1F1F9-1F1F1", + "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" + }, + { + "name": "flag_tm", + "unicode": "1F1F9-1F1F2", + "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" + }, + { + "name": "flag_tn", + "unicode": "1F1F9-1F1F3", + "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" + }, + { + "name": "flag_to", + "unicode": "1F1F9-1F1F4", + "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" + }, + { + "name": "flag_tr", + "unicode": "1F1F9-1F1F7", + "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" + }, + { + "name": "flag_tt", + "unicode": "1F1F9-1F1F9", + "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" + }, + { + "name": "flag_tv", + "unicode": "1F1F9-1F1FB", + "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" + }, + { + "name": "flag_tw", + "unicode": "1F1F9-1F1FC", + "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" + }, + { + "name": "flag_tz", + "unicode": "1F1F9-1F1FF", + "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" + }, + { + "name": "flag_ua", + "unicode": "1F1FA-1F1E6", + "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" + }, + { + "name": "flag_ug", + "unicode": "1F1FA-1F1EC", + "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" + }, + { + "name": "flag_um", + "unicode": "1F1FA-1F1F2", + "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" + }, + { + "name": "flag_us", + "unicode": "1F1FA-1F1F8", + "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" + }, + { + "name": "flag_uy", + "unicode": "1F1FA-1F1FE", + "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" + }, + { + "name": "flag_uz", + "unicode": "1F1FA-1F1FF", + "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" + }, + { + "name": "flag_va", + "unicode": "1F1FB-1F1E6", + "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" + }, + { + "name": "flag_vc", + "unicode": "1F1FB-1F1E8", + "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" + }, + { + "name": "flag_ve", + "unicode": "1F1FB-1F1EA", + "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" + }, + { + "name": "flag_vg", + "unicode": "1F1FB-1F1EC", + "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" + }, + { + "name": "flag_vi", + "unicode": "1F1FB-1F1EE", + "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" + }, + { + "name": "flag_vn", + "unicode": "1F1FB-1F1F3", + "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" + }, + { + "name": "flag_vu", + "unicode": "1F1FB-1F1FA", + "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" + }, + { + "name": "flag_wf", + "unicode": "1F1FC-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, + { + "name": "flag_white", + "unicode": "1F3F3", + "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" + }, + { + "name": "flag_ws", + "unicode": "1F1FC-1F1F8", + "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" + }, + { + "name": "flag_xk", + "unicode": "1F1FD-1F1F0", + "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" + }, + { + "name": "flag_ye", + "unicode": "1F1FE-1F1EA", + "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" + }, + { + "name": "flag_yt", + "unicode": "1F1FE-1F1F9", + "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" + }, + { + "name": "flag_za", + "unicode": "1F1FF-1F1E6", + "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" + }, + { + "name": "flag_zm", + "unicode": "1F1FF-1F1F2", + "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" + }, + { + "name": "flag_zw", + "unicode": "1F1FF-1F1FC", + "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" + }, + { + "name": "flags", + "unicode": "1F38F", + "digest": "c3f4a66786e524a5562919afcba9486113091ed205f1342e91d2f6439845ad61" + }, + { + "name": "flashlight", + "unicode": "1F526", + "digest": "5f641b8fd1c7f1dcd43ec3b1ef78d14ef9929d723789c5567aca8b95d3d39803" + }, + { + "name": "fleur-de-lis", + "unicode": "269C", + "digest": "d6ddeeea355ed55103b7fc65ac1ee0dbaa79d01e0d136b265363a6b92284c073" + }, + { + "name": "flip_phone", + "unicode": "1F581", + "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" + }, + { + "name": "floppy_black", + "unicode": "1F5AA", + "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" + }, + { + "name": "floppy_disk", + "unicode": "1F4BE", + "digest": "e987961ca516032a90942ef6c398836f2da68a5981714bd172acfe7b0e369d0a" + }, + { + "name": "floppy_white", + "unicode": "1F5AB", + "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" + }, + { + "name": "flower_playing_cards", + "unicode": "1F3B4", + "digest": "451f361050b96ba9ed8dc5b64c8a90c1316fd9b83fb818152881a54e100eea6c" + }, + { + "name": "flushed", + "unicode": "1F633", + "digest": "39cf51f9dec2a910c66ecd39a7bd616fea09d67e81801e57e84f03ed1e917750" + }, + { + "name": "fog", + "unicode": "1F32B", + "digest": "da6fdb9b682ed9a3368adcd7531f1a29e22755a620e3cca163fc3f33a6a78107" + }, + { + "name": "foggy", + "unicode": "1F301", + "digest": "b599f3178db289c6e30017f3f0a9d30b00a75417057c7a10c0c9eedac78edbf1" + }, + { + "name": "folder", + "unicode": "1F5C0", + "digest": "8932141321911032ce8469ba85fe309b78384545c3b9946978b383670b956644" + }, + { + "name": "folder_open", + "unicode": "1F5C1", + "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" + }, + { + "name": "football", + "unicode": "1F3C8", + "digest": "834fe5f431d6aa8ef1186aa79e71f813393535d273483b6af4cc4bdb8380e5b4" + }, + { + "name": "footprints", + "unicode": "1F463", + "digest": "60dc938f6769ea21b05b5afcc481d3ddacf1f565e04f33310b271d5422e7ceb9" + }, + { + "name": "fork_and_knife", + "unicode": "1F374", + "digest": "7e07c9dc555d172fa2eaa41cefd8d46d9624be0137aff196dd003a8a82610ec3" + }, + { + "name": "fork_knife_plate", + "unicode": "1F37D", + "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" + }, + { + "name": "fountain", + "unicode": "26F2", + "digest": "0acdca5e8f6d745a8d582d96012ec8fc55b9f5447e657ebfd998a4e332d99322" + }, + { + "name": "four", + "unicode": "0034-20E3", + "digest": "36bd4ea6e2ae689835a79f8e60466eccd62fce7e91e84ed768cffd87dac628dd" + }, + { + "name": "four_leaf_clover", + "unicode": "1F340", + "digest": "12ee2343df25bbd9077fdc12314c1edb51c0cdb556af7e22590e8a578ef57f17" + }, + { + "name": "frame_photo", + "unicode": "1F5BC", + "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" + }, + { + "name": "frame_tiles", + "unicode": "1F5BD", + "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" + }, + { + "name": "frame_x", + "unicode": "1F5BE", + "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" + }, + { + "name": "free", + "unicode": "1F193", + "digest": "c1d9172a656717f78d941303c5da8790c6cd9827838d8f7dc3719afb53bcab80" + }, + { + "name": "fried_shrimp", + "unicode": "1F364", + "digest": "c0c19e95f2c38f6cf870920bf3c2d4d69c36ea6e7dc9a5c45c3e8b285269d40a" + }, + { + "name": "fries", + "unicode": "1F35F", + "digest": "0f546534684de29d319cbcbab4162acb321c4f8f3202fe17d69e1894ab7c8195" + }, + { + "name": "frog", + "unicode": "1F438", + "digest": "6a417757fa6ee39e7a277cbd53c690ff88af0b1d76728d56f9bc645cb628aeb7" + }, + { + "name": "frowning", + "unicode": "1F626", + "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" + }, + { + "name": "frowning2", + "unicode": "2639", + "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" + }, + { + "name": "fuelpump", + "unicode": "26FD", + "digest": "9cbb2646c93b255bd3de87dc01aa1193ab96e39a3013975d250472ab8aae61d6" + }, + { + "name": "full_moon", + "unicode": "1F315", + "digest": "0b4f08ef2089397ead034b444a60e6e9810073454581b52a46b2369e3b9cd5f9" + }, + { + "name": "full_moon_with_face", + "unicode": "1F31D", + "digest": "a371cb9e1f28a7db739dd058234642a2e333dff4b6df9882df85a6d984e4b5e8" + }, + { + "name": "game_die", + "unicode": "1F3B2", + "digest": "6584909a4348c350c04417421b63eace1245087f7d239051b30a0cd37fe929f9" + }, + { + "name": "gear", + "unicode": "2699", + "digest": "b0ff5fd007daa366a9eecb7422dbeb8a973e123a04267b88fef96c7453238294" + }, + { + "name": "gem", + "unicode": "1F48E", + "digest": "d75d854f35975e4e291c3b9fcaf8437467f6d7eb27b29e2d7c0f0038fc666fe2" + }, + { + "name": "gemini", + "unicode": "264A", + "digest": "392abe62872736a0bf92979a8c25a814985d0ff0a08dc7ab2a5c058aeda7e685" + }, + { + "name": "ghost", + "unicode": "1F47B", + "digest": "f084b14483476e2d07563840f8c33b46da9c17f791da07fde3acffeb77342947" + }, + { + "name": "gift", + "unicode": "1F381", + "digest": "c9a2ae6ea05c02e78e9567dcbd971701a2f869eb46c62d85cef23d0834388d8c" + }, + { + "name": "gift_heart", + "unicode": "1F49D", + "digest": "e0c5aacf1ce89117d86b148f10a02dc18fe0cd22a75fbf6f0f88f2fad3ca80fe" + }, + { + "name": "girl", + "unicode": "1F467", + "digest": "0758cbc4cbc7d72d6df8f66fc3a6b2b283c6634b053e59d61c6cac44cf8bffda" + }, + { + "name": "girl_tone1", + "unicode": "1F467-1F3FB", + "digest": "7afdece55cb64e8056e2202de8c17b66ddb616f224ac374ec9a160d06b3138cc" + }, + { + "name": "girl_tone2", + "unicode": "1F467-1F3FC", + "digest": "c160aa65fee70ad52930d01246ac9f282ff6abf1d93c5cc5b299fc257ee81db1" + }, + { + "name": "girl_tone3", + "unicode": "1F467-1F3FD", + "digest": "b8a5687cd637855a41b8c7dc686f0e69fda379875408cd269f1b330a805c72f4" + }, + { + "name": "girl_tone4", + "unicode": "1F467-1F3FE", + "digest": "a9cf743936b733634f323790a1abe3a410601b6841484baebea484b392f4e98e" + }, + { + "name": "girl_tone5", + "unicode": "1F467-1F3FF", + "digest": "c902170e67b81eee35eeefb6a5c62c6109cb423dcae88d4e036ddd50b240c072" + }, + { + "name": "girls_symbol", + "unicode": "1F6CA", + "digest": "2c55aee81defd7a1620ffeaad8d9bcc1835f19237c72c79633aec45671ddb9ff" + }, + { + "name": "globe_with_meridians", + "unicode": "1F310", + "digest": "945646de3d8f057760fe374494a253d9a6aa8a132309154b0a5bdbffb5b20c3f" + }, + { + "name": "goat", + "unicode": "1F410", + "digest": "f99cbc6755d119cb5c1dce08cabd20871f98d009bb773da4a146dae60476a235" + }, + { + "name": "golf", + "unicode": "26F3", + "digest": "74a7876d185f8ff6a6533e4db2e1eb787119b2f8d8b07c36d99ec3163fb48485" + }, + { + "name": "golfer", + "unicode": "1F3CC", + "digest": "6458295a5e4a6e4323c32a7f1f7182fb2d3918083839efc380d995860ce360b1" + }, + { + "name": "grapes", + "unicode": "1F347", + "digest": "7f6873d65180ab476f49d207ac2d1f7dbaf6c8b0b561d50b64325e192cf97a86" + }, + { + "name": "green_apple", + "unicode": "1F34F", + "digest": "effc3fe60f2ab704a034c794bfccfa023b41332f8f16ca44cc8ea41698f03873" + }, + { + "name": "green_book", + "unicode": "1F4D7", + "digest": "6652c4d2ccfa4a287a5d45007bd06cadc16d34b0a1ca4b6b13b46f976c8d8319" + }, + { + "name": "green_heart", + "unicode": "1F49A", + "digest": "f4bcb660a1d3cf3692238359d8b9de9a725a9af81f166253e487d61b8ccf9d86" + }, + { + "name": "grey_exclamation", + "unicode": "2755", + "digest": "ac8cdab7496d133e7bc9475f2fdb0cf59b3ccba20f2f156c8b693e72b5948078" + }, + { + "name": "grey_question", + "unicode": "2754", + "digest": "c173e1b2a16ab62b0abd7a58deb7a6df709b072d30d001627b92d0123a3a3e4a" + }, + { + "name": "grimacing", + "unicode": "1F62C", + "digest": "8c54b73f5d2c1c6347e2c0ab01616519e0fb34490daa9c36664d442c6851c57e" + }, + { + "name": "grin", + "unicode": "1F601", + "digest": "916eabdabd8b7ca698e638bbbd14affff97464ec11a3b59c0cb96cd7705600d8" + }, + { + "name": "grinning", + "unicode": "1F600", + "digest": "3d8665c03f272ca3063e96145989926355a7ac315ed1a032d30fcefa6f0c3923" + }, + { + "name": "guardsman", + "unicode": "1F482", + "digest": "ebbd29fa138005232d64fca4a8ec015d097fa14e6ded57b35ac257b4570b3c36" + }, + { + "name": "guardsman_tone1", + "unicode": "1F482-1F3FB", + "digest": "b6082c8fee5dbc3ce2540f3939d5e344b5366c9f07827345facaba438e7017ff" + }, + { + "name": "guardsman_tone2", + "unicode": "1F482-1F3FC", + "digest": "2b813afe1c2bbdaf9a47493393a0e6c400a16e453ed25a9a9c0035197927b56e" + }, + { + "name": "guardsman_tone3", + "unicode": "1F482-1F3FD", + "digest": "49b2fa1ad0bc50a5ef6d73fb140aa1876506b9ebb9d45782ccb8dbb6818f8dde" + }, + { + "name": "guardsman_tone4", + "unicode": "1F482-1F3FE", + "digest": "a584e1e3a8ad7be4871a6bdb7996d4f649abeaa77eb5d1cae998058d8b23ca0f" + }, + { + "name": "guardsman_tone5", + "unicode": "1F482-1F3FF", + "digest": "e853b67ee13fda99e98f47083529ca80c404df1b19352c78b9c69850eb8f2c76" + }, + { + "name": "guitar", + "unicode": "1F3B8", + "digest": "8c041b961649cc5917f56f2fb543f9a5280724647ed2fc67bc94a05eff9da805" + }, + { + "name": "gun", + "unicode": "1F52B", + "digest": "d7f5aa657cc0ba04d878511820632b89c305a9b4d6c4a4b90ff691dad9906607" + }, + { + "name": "haircut", + "unicode": "1F487", + "digest": "369dbab1b138c31d3eca04c950fdab4ec9f085272268c241f100d44e7b0f229e" + }, + { + "name": "haircut_tone1", + "unicode": "1F487-1F3FB", + "digest": "c56f32d7c1d8a92d22429133f87f31a159818939cfdc570cb48b6d243cc58cf2" + }, + { + "name": "haircut_tone2", + "unicode": "1F487-1F3FC", + "digest": "e916e040ffb8e869e930d1256343af2ad2bbaa683f01a11564d0777019944bec" + }, + { + "name": "haircut_tone3", + "unicode": "1F487-1F3FD", + "digest": "f07cdfbea964ac42a9a050f832107ef0f2fa8115b27689f93d1be954de07b7c1" + }, + { + "name": "haircut_tone4", + "unicode": "1F487-1F3FE", + "digest": "32ec7f5e999f7c43676768c8320ffaa346c713d340a94b948b1f564b345a2d11" + }, + { + "name": "haircut_tone5", + "unicode": "1F487-1F3FF", + "digest": "5aad997d09e7975700927906d41a10bae774356ccddbe5197980bde670272262" + }, + { + "name": "hamburger", + "unicode": "1F354", + "digest": "24ebae9a69cf283ab198499cb38d0cdcd82bac74c8e8d1e769ad78eb320a4294" + }, + { + "name": "hammer", + "unicode": "1F528", + "digest": "a43a66b0efdc4cd2c84fd0ccc2cb8e9ede1f89c5d62eefa6ae521d3aed9d81b3" + }, + { + "name": "hammer_pick", + "unicode": "2692", + "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" + }, + { + "name": "hamster", + "unicode": "1F439", + "digest": "f47da088ff5792532a382b6e3a47d2dd7c5e6fc19abd5ff6c5ba3ce420b4192e" + }, + { + "name": "hand_splayed", + "unicode": "1F590", + "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" + }, + { + "name": "hand_splayed_reverse", + "unicode": "1F591", + "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" + }, + { + "name": "hand_splayed_tone1", + "unicode": "1F590-1F3FB", + "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" + }, + { + "name": "hand_splayed_tone2", + "unicode": "1F590-1F3FC", + "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" + }, + { + "name": "hand_splayed_tone3", + "unicode": "1F590-1F3FD", + "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" + }, + { + "name": "hand_splayed_tone4", + "unicode": "1F590-1F3FE", + "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" + }, + { + "name": "hand_splayed_tone5", + "unicode": "1F590-1F3FF", + "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" + }, + { + "name": "hand_victory", + "unicode": "1F594", + "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" + }, + { + "name": "handbag", + "unicode": "1F45C", + "digest": "f1e2822c67f659b52c76821dd9db001332215a8566fc1846c89b6019c9758038" + }, + { + "name": "hard_disk", + "unicode": "1F5B4", + "digest": "df8549d4281f5ae70fb6792a02c078e651764b0276aa43b7407236bd38fc21b4" + }, + { + "name": "hash", + "unicode": "0023-20E3", + "digest": "5bd5c7180485fa71accdec5378bdc196ce0602f594f91e4eadc1e7514d5d0f90" + }, + { + "name": "hatched_chick", + "unicode": "1F425", + "digest": "7995c3eb503a8b9662694eba80a9b551216473a31928091e35cd6ebc21cee083" + }, + { + "name": "hatching_chick", + "unicode": "1F423", + "digest": "22905b42fa65dbc9aad8940d2db13691cacc62014f54e0960978ee0002178e1b" + }, + { + "name": "head_bandage", + "unicode": "1F915", + "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" + }, + { + "name": "headphones", + "unicode": "1F3A7", + "digest": "219da138032c01c97a94f02b211049418191a3beb3d159804b9033f5916fd3c8" + }, + { + "name": "hear_no_evil", + "unicode": "1F649", + "digest": "8120060238eaca645809dd113862a144f10395afcb3837ab60c0f04009b49a2f" + }, + { + "name": "heart", + "unicode": "2764", + "digest": "a646a25a36f431cadc7e56afd1a4d1b7cbae5292a25d7783bd31462d0d3d719b" + }, + { + "name": "heart_decoration", + "unicode": "1F49F", + "digest": "a83989669347c98cb74065d4f0befedbc37f82c91214e773245cb6810ab359b4" + }, + { + "name": "heart_exclamation", + "unicode": "2763", + "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" + }, + { + "name": "heart_eyes", + "unicode": "1F60D", + "digest": "335ea73efca4824e623a5a51ccdb494c8b1f5f10b4139b39b250a2a771876b0d" + }, + { + "name": "heart_eyes_cat", + "unicode": "1F63B", + "digest": "9346b85afb80f7b498cc255426ea15a287f81d8fb3c26dab61337635f439d3ce" + }, + { + "name": "heart_tip", + "unicode": "1F394", + "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" + }, + { + "name": "heartbeat", + "unicode": "1F493", + "digest": "cd6921ce55c155873220a09416d695c4bcca1556007066d6d185e93d6561e825" + }, + { + "name": "heartpulse", + "unicode": "1F497", + "digest": "f869357b9e678d9671ec38c569fc88efec48006c159b69297277cee795dc4dc9" + }, + { + "name": "hearts", + "unicode": "2665", + "digest": "17dc9b2941561f58ca0f04d0754b1eff3490b63b17241580b3d4aa4638fa85e8" + }, + { + "name": "heavy_check_mark", + "unicode": "2714", + "digest": "b5fa24f6e0f1dcbd6278e9125154522f2efd79e6dd0836ccb792a1f3aeeff2b2" + }, + { + "name": "heavy_division_sign", + "unicode": "2797", + "digest": "59a6983d788f347c64eecb3df6f7d3b36779d92df6cc811820993ff9e18d77e1" + }, + { + "name": "heavy_dollar_sign", + "unicode": "1F4B2", + "digest": "d2e89c54b3fdeda4d1fd4d29454b69dcf750181110894e6e71a40df99c95bfe8" + }, + { + "name": "heavy_minus_sign", + "unicode": "2796", + "digest": "dd5ab3722fe49cfdbc5e1fbab5b342dc960de7b412d4fba59d66e06ce3dc3bcd" + }, + { + "name": "heavy_multiplication_x", + "unicode": "2716", + "digest": "7d77742f91377785675802f40bd8dde9bd1feeb513735760a58ea9bee8a65d44" + }, + { + "name": "heavy_plus_sign", + "unicode": "2795", + "digest": "9aa9dcdbba120a4b485c21f67589609b789c6e3edf08479ff8268fa0db973ad7" + }, + { + "name": "helicopter", + "unicode": "1F681", + "digest": "b259ea8d2bdca36766075894da650b1d3ff4c8602259cd0d30cb8214cd585340" + }, + { + "name": "helmet_with_cross", + "unicode": "26D1", + "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" + }, + { + "name": "herb", + "unicode": "1F33F", + "digest": "3c452106b1966f643751bf161fa7d1762a33e6fff381b2109bb53b55c4fdd129" + }, + { + "name": "hibiscus", + "unicode": "1F33A", + "digest": "268963a1f3cdad9050d9ae31c558e010f33812e3b09bbf9088ba876c033d8b2f" + }, + { + "name": "high_brightness", + "unicode": "1F506", + "digest": "d607f6269d95dd16c2a7932e49ac09e44f4c19e0a34f6c0f21ecb945a2316361" + }, + { + "name": "high_heel", + "unicode": "1F460", + "digest": "5c320d5954bf4f4dacacddd562c1598ab101731077a6656ac5d2bfd41405483e" + }, + { + "name": "hockey", + "unicode": "1F3D2", + "digest": "008904c1b8db139215492a6d96c09f2c3eeda769f858a9bbae13f8c54d439d0e" + }, + { + "name": "hole", + "unicode": "1F573", + "digest": "36bbafa5e89b1410ec74919aaf60b09ac3525a421cb5b475b9bb2f20357db8de" + }, + { + "name": "homes", + "unicode": "1F3D8", + "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" + }, + { + "name": "honey_pot", + "unicode": "1F36F", + "digest": "94cb1624491076b5cb145e7a309f91a7be3d4c0bed712af6a51d641eb73edee7" + }, + { + "name": "horse", + "unicode": "1F434", + "digest": "624ad9dc9ed7af3f6e1a2f9d4ed483702ae64ed5fbcf5e9918af6bfef24e76f9" + }, + { + "name": "horse_racing", + "unicode": "1F3C7", + "digest": "c2702b7225e9839a789dda7c43f0cc86dced2b4d5d3787116106396633362de6" + }, + { + "name": "horse_racing_tone1", + "unicode": "1F3C7-1F3FB", + "digest": "a7ed284f9d5cd8a4fe4a09cb91c3f99e5db99c7e31c5f525c14de97b06857d92" + }, + { + "name": "horse_racing_tone2", + "unicode": "1F3C7-1F3FC", + "digest": "20b4d61b21ee6ba860b029f0ad0e38f5ecb6dd2c774f7b7801fba07ed33f96be" + }, + { + "name": "horse_racing_tone3", + "unicode": "1F3C7-1F3FD", + "digest": "dd65f7bb96ee44507d26e524202d567d2d7679d571245299a2a84f68bd5def4c" + }, + { + "name": "horse_racing_tone4", + "unicode": "1F3C7-1F3FE", + "digest": "36afaad218a4c820b19c7c9bbbc187119d47b41273d8f48ab14cc3e32dd7c21f" + }, + { + "name": "horse_racing_tone5", + "unicode": "1F3C7-1F3FF", + "digest": "2e0efd501a4471428533ce7909972a49ff045369261c27e4abb97ee2aede2f47" + }, + { + "name": "hospital", + "unicode": "1F3E5", + "digest": "df5c774fa36b2601e6960a7b81cdfac71c1d2d71f04dea88068d1c9043e313bb" + }, + { + "name": "hot_pepper", + "unicode": "1F336", + "digest": "62e4dade3c793f6d83530bd1f60f3e3e26c1e10a41786c3a15f5aec0ff2b8e76" + }, + { + "name": "hotdog", + "unicode": "1F32D", + "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" + }, + { + "name": "hotel", + "unicode": "1F3E8", + "digest": "428120a35b38a217901e10d704751eb8fdbc9f805e6eccd8aab070f4311b2085" + }, + { + "name": "hotsprings", + "unicode": "2668", + "digest": "df4f946218445f97a6f28c6abe4c1d1dac56ff97a8cd81df59f1b3c320e0092f" + }, + { + "name": "hourglass", + "unicode": "231B", + "digest": "07aece9413e6898717b4f0757e073d7a593f3e8044c56855127033b796207ccb" + }, + { + "name": "hourglass_flowing_sand", + "unicode": "23F3", + "digest": "92dbc68e9d16fb9f706236367e1882f0d2b6817b83ca490820a000021f2c6483" + }, + { + "name": "house", + "unicode": "1F3E0", + "digest": "a6221fc84a9b0e11ae71bfa1e0020982b55ff8c89a374a6d755dba710b4e058c" + }, + { + "name": "house_abandoned", + "unicode": "1F3DA", + "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" + }, + { + "name": "house_with_garden", + "unicode": "1F3E1", + "digest": "22d0d911da96b7ae3bf6692d3cf3590afbca959fc99c13e7a088f7194f43a35d" + }, + { + "name": "hugging", + "unicode": "1F917", + "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" + }, + { + "name": "hushed", + "unicode": "1F62F", + "digest": "69faa8e0b170ee8cf41977ca4a5154406360ed9699d5c62ecdaa01f50e8e4276" + }, + { + "name": "ice_cream", + "unicode": "1F368", + "digest": "d48ec98a8789148b96c30f19595201a0f85ed899659d97d1d3596091162909ff" + }, + { + "name": "ice_skate", + "unicode": "26F8", + "digest": "6fb044d9fbe62605f6728062c35c345ddd3ae4cc51203c925b0e69f1b3ef2dbf" + }, + { + "name": "icecream", + "unicode": "1F366", + "digest": "abd5774157575dd304dc1a393244757853972c863861a654ca29b2d528e48b28" + }, + { + "name": "id", + "unicode": "1F194", + "digest": "860ffb36d37d84e2c1cf0ab991b95c1cf73e458bef0e4d85bb0c1e26115cb2d1" + }, + { + "name": "ideograph_advantage", + "unicode": "1F250", + "digest": "37892a5642cd49ef7828646f36f48b5a83dc02437624c05da428579256118030" + }, + { + "name": "imp", + "unicode": "1F47F", + "digest": "f8c93d03bd9f1d5ef86738541e11695d6811bf6fef06759eba98321b6d038814" + }, + { + "name": "inbox_tray", + "unicode": "1F4E5", + "digest": "066a2d75633eb50329496f6866b5b0645c2e48135a03118f1bf53244f8529043" + }, + { + "name": "incoming_envelope", + "unicode": "1F4E8", + "digest": "ef6e5c5aa679d174181dae77113717f26e295778dde1e2c3bdf1d64de8a4af8c" + }, + { + "name": "info", + "unicode": "1F6C8", + "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" + }, + { + "name": "information_desk_person", + "unicode": "1F481", + "digest": "acae6d272e348aee87dd60360f16ac58cea7cb4e1ea962cc1655005c7f4aed27" + }, + { + "name": "information_desk_person_tone1", + "unicode": "1F481-1F3FB", + "digest": "709ebb0481ca981d76ece2d4fc68db693ddf18b9c1aaa0b6ac5d3c42e71bf07f" + }, + { + "name": "information_desk_person_tone2", + "unicode": "1F481-1F3FC", + "digest": "d5bc3563bc721d66b73850db93ac827be3715e7ca6420dc0051396ffe26bef47" + }, + { + "name": "information_desk_person_tone3", + "unicode": "1F481-1F3FD", + "digest": "af67fd4ef2fc402bec2d446b2e8ff5e9f636b5a9bbb6639587cdb88bd780d265" + }, + { + "name": "information_desk_person_tone4", + "unicode": "1F481-1F3FE", + "digest": "fd3174d1adfe13e8c0d6b6ae9c3a26ea35bb40f98f0728f91d1798809a74933b" + }, + { + "name": "information_desk_person_tone5", + "unicode": "1F481-1F3FF", + "digest": "4b773c443830a02de8b4d6471077b5d1387b560b537cabba7cdc667110cbde69" + }, + { + "name": "information_source", + "unicode": "2139", + "digest": "50cd8bf46d20b7c18d5f00a69fc79452aa32934245ba8d0929e51632d73876bd" + }, + { + "name": "innocent", + "unicode": "1F607", + "digest": "a3510fd51c17093ebe2371cfde7611aa44aed2d120a0e5500cfaae0f1d3486a4" + }, + { + "name": "interrobang", + "unicode": "2049", + "digest": "1f843ff672486154f9f3df549bb1b528a5eac8d15264f447649ba57f45ee4d00" + }, + { + "name": "iphone", + "unicode": "1F4F1", + "digest": "be6f96c02ddae557f700fd20fe7b3f94c9e1c928acb82b2b8b214d231273fece" + }, + { + "name": "island", + "unicode": "1F3DD", + "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" + }, + { + "name": "izakaya_lantern", + "unicode": "1F3EE", + "digest": "ddb20f475aa119c3a64a55dff40f7a9dbc3a14f7ffc6cfbac89210c652f10d02" + }, + { + "name": "jack_o_lantern", + "unicode": "1F383", + "digest": "62a701ac472619bcb3859e0d9a61b98c7f5c32150d2d04ca8c3e8fc3bec4dbd5" + }, + { + "name": "japan", + "unicode": "1F5FE", + "digest": "2535300fff2b2e4b75fc73c187be6c0ea4bc4753e443db498ea55e268e627ab7" + }, + { + "name": "japanese_castle", + "unicode": "1F3EF", + "digest": "70645aa05599e23a9ac4327e4a2e78bffe7ea06c38ec1935c15ae420619c5c1c" + }, + { + "name": "japanese_goblin", + "unicode": "1F47A", + "digest": "59b6901dc6eedc6509c25b4eef6702bf461ded06c5ff12fe2a02a5b3301577c0" + }, + { + "name": "japanese_ogre", + "unicode": "1F479", + "digest": "dab7e68cd4cbf99c13d64792c7104c4f0a846bc63aa12950fa8fab028dca301d" + }, + { + "name": "jeans", + "unicode": "1F456", + "digest": "ddd032ac77cdfe49152a0e0a0eaaaea9f183590fb1f493ec30e9e39f679e3914" + }, + { + "name": "jet_up", + "unicode": "1F6E6", + "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" + }, + { + "name": "joy", + "unicode": "1F602", + "digest": "f90cfbcb14f906f8d786b61f022c978f381fc99ca422805f605631314e101805" + }, + { + "name": "joy_cat", + "unicode": "1F639", + "digest": "6ca24a94490de66d1ca2cbc080bcd805f54ca295051d8e6588cae3fe6658c80a" + }, + { + "name": "joystick", + "unicode": "1F579", + "digest": "ec172df88ef8e8a5512d6d906c13296875b7057ed0cca79f4ac8cddd9e1de34b" + }, + { + "name": "kaaba", + "unicode": "1F54B", + "digest": "30f1a27a148399bbb811586eff795eff858701c42055c23e4d5bef7ae77f5f32" + }, + { + "name": "key", + "unicode": "1F511", + "digest": "c68ed648350d3976c8d27a709020c8873ecf553929e66453acff96231684a1a2" + }, + { + "name": "key2", + "unicode": "1F5DD", + "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" + }, + { + "name": "keyboard", + "unicode": "1F5AE", + "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" + }, + { + "name": "keyboard_mouse", + "unicode": "1F5A6", + "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" + }, + { + "name": "keyboard_with_jacks", + "unicode": "1F398", + "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" + }, + { + "name": "keycap_ten", + "unicode": "1F51F", + "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03" + }, + { + "name": "kimono", + "unicode": "1F458", + "digest": "e92bea044fe013f1993c2229d86e9cca9d43f14aab00564ce6ff559bdc5ce93a" + }, + { + "name": "kiss", + "unicode": "1F48B", + "digest": "c060eb09af2a0d0f77d307b995c15719b0e59c9162a490b8a553fac9b779c8f0" + }, + { + "name": "kiss_mm", + "unicode": "1F468-2764-1F48B-1F468", + "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" + }, + { + "name": "kiss_ww", + "unicode": "1F469-2764-1F48B-1F469", + "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" + }, + { + "name": "kissing", + "unicode": "1F617", + "digest": "3142617e8b9488689bd9efc67c0e4cc71a1870df8ffc308f949eedc5c3684051" + }, + { + "name": "kissing_cat", + "unicode": "1F63D", + "digest": "ed26cee8c438ba41365b55c48457cdad3e8d43bf90db3128ac5b277718b82ed3" + }, + { + "name": "kissing_closed_eyes", + "unicode": "1F61A", + "digest": "22d3369d21b4c2cb4c0c2cab9551cd848dd4f9adecfa64977d3f1a80fc0c8b53" + }, + { + "name": "kissing_heart", + "unicode": "1F618", + "digest": "1f089b07447bdcc1baada6a2a9607d4ef4f2de9a6093fcab47a553a64b9acb76" + }, + { + "name": "kissing_smiling_eyes", + "unicode": "1F619", + "digest": "e37d282861669adfa3953b9af833acfab7d55e787621d4318d77de7e3529d5c5" + }, + { + "name": "knife", + "unicode": "1F52A", + "digest": "3fef068a6ada61630dc868e47d25e0e0550b44bc7cf530afe88ca63dc7ab2a39" + }, + { + "name": "koala", + "unicode": "1F428", + "digest": "fe020ab9048f3c2a881474f8b1335db6bfaf37d115ff9b2d264f668d136122dd" + }, + { + "name": "koko", + "unicode": "1F201", + "digest": "734a5cb296826a598e02be3f4ec22f318633ede2ce274914586256421e2df97b" + }, + { + "name": "label", + "unicode": "1F3F7", + "digest": "9fe8195c3efab4d905b1cfcba0ae58cda12496030b0908de8076ff5e6777742e" + }, + { + "name": "large_blue_circle", + "unicode": "1F535", + "digest": "ba4d0f84a9c2be9a65b25c8cfa78f30d4856d021b1853154dd1d2fd0c5bcfb6a" + }, + { + "name": "large_blue_diamond", + "unicode": "1F537", + "digest": "d5aa5e315126859c10c83507be6b9e11cbf423f7a27145de089468cff9b94a94" + }, + { + "name": "large_orange_diamond", + "unicode": "1F536", + "digest": "108600badd0ef267842325c0fbf326cb3504306332c64f6f5694de2b54c9438a" + }, + { + "name": "last_quarter_moon", + "unicode": "1F317", + "digest": "68315b85bc1cb17bb82629bd1a6024a5124f3641b9878a732a8aad016c587546" + }, + { + "name": "last_quarter_moon_with_face", + "unicode": "1F31C", + "digest": "146a419109b7f662bf87cf9de299e47d025a8758c8970b7dabf3483e1956b559" + }, + { + "name": "laughing", + "unicode": "1F606", + "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" + }, + { + "name": "leaves", + "unicode": "1F343", + "digest": "f65e2db125564eb04fc427a49fff175d6e2dae847bd12314d5e6a131610d5ccd" + }, + { + "name": "ledger", + "unicode": "1F4D2", + "digest": "62df1772cec10c035ae0646e6cca4ba7d75b10636a520d091c5b42c2dc36b742" + }, + { + "name": "left_luggage", + "unicode": "1F6C5", + "digest": "62292758715115e55ab6239805b7f99b7b35bdfa8d40da07fe391424f1f083d8" + }, + { + "name": "left_receiver", + "unicode": "1F57B", + "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" + }, + { + "name": "left_right_arrow", + "unicode": "2194", + "digest": "28a6945972451b1f4dadec5c55310b8868ffd9f3b0a07803287bc4e07a56e7d4" + }, + { + "name": "leftwards_arrow_with_hook", + "unicode": "21A9", + "digest": "d672afc39fd50f78d7370be243173fe76ba50292f0c401305b562898939a8b7f" + }, + { + "name": "lemon", + "unicode": "1F34B", + "digest": "e0e293a8b8c1b3c87534f5e05cf006671eb3c6d52b4d17d40f2e23bce215a8be" + }, + { + "name": "leo", + "unicode": "264C", + "digest": "b0fd4e5f4637de530b62323521c6edcd80312d67ea4043eedd959acb6763474a" + }, + { + "name": "leopard", + "unicode": "1F406", + "digest": "ede891be8484a17e6277431c64ec1bfd6b742544a41947ebc85005bc2d558bb1" + }, + { + "name": "level_slider", + "unicode": "1F39A", + "digest": "49777cf160d9130d723e3bfef765c3de54033e6b059000fb0e22fb559b5ed190" + }, + { + "name": "levitate", + "unicode": "1F574", + "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" + }, + { + "name": "libra", + "unicode": "264E", + "digest": "ec8e2e7a735abc9f2bddb115fc0e09f4bdc7a164679e2b57d127f58eee1155c2" + }, + { + "name": "lifter", + "unicode": "1F3CB", + "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" + }, + { + "name": "lifter_tone1", + "unicode": "1F3CB-1F3FB", + "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" + }, + { + "name": "lifter_tone2", + "unicode": "1F3CB-1F3FC", + "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" + }, + { + "name": "lifter_tone3", + "unicode": "1F3CB-1F3FD", + "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" + }, + { + "name": "lifter_tone4", + "unicode": "1F3CB-1F3FE", + "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" + }, + { + "name": "lifter_tone5", + "unicode": "1F3CB-1F3FF", + "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" + }, + { + "name": "light_check_mark", + "unicode": "1F5F8", + "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" + }, + { + "name": "light_rail", + "unicode": "1F688", + "digest": "7c2be55456f1332e849ff6699a26dda2e1641c280f45c9ec88dedf6d9b7b7fe2" + }, + { + "name": "link", + "unicode": "1F517", + "digest": "cc4873f8a612dd721dddcd507a4430b4fb6c4abc15a8848456f0ffd97811b163" + }, + { + "name": "lion_face", + "unicode": "1F981", + "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" + }, + { + "name": "lips", + "unicode": "1F444", + "digest": "e3bc20f9e210fa1711271234fe61bf1c9ddf36dd6ffc5b832c6c3a769a1e59a8" + }, + { + "name": "lips2", + "unicode": "1F5E2", + "digest": "c6ba915982ac47d8aaf14ad3605949df95588acfb4e147bf608f8c1714cdf19b" + }, + { + "name": "lipstick", + "unicode": "1F484", + "digest": "335b912e163020df3d6d9f0a19a55d6547bd59b471c5a3e374c2968e49911ccc" + }, + { + "name": "lock", + "unicode": "1F512", + "digest": "c20eacfb8ccd9bb85919a837c0d4650ee608edb48c85bff46945f613e95d7038" + }, + { + "name": "lock_with_ink_pen", + "unicode": "1F50F", + "digest": "5cab25cea08e22d9c3f5de16de6d0ab658ca15cc93d7830f29b0f3e9348ec45f" + }, + { + "name": "lollipop", + "unicode": "1F36D", + "digest": "33d2334a00bf0e15869ccc75fadc36f27f89abf0525bb71f859aad9e1dc4ad66" + }, + { + "name": "loop", + "unicode": "27BF", + "digest": "fa1174ddc44e317d0796e07868c7ac8ac9c9274fbc8a6c3d0ec78d543c3c6bf0" + }, + { + "name": "loud_sound", + "unicode": "1F50A", + "digest": "fb70229e13b690ffc1031d2e631123f8c908035a15218c297c1c4a3ff3624aa0" + }, + { + "name": "loudspeaker", + "unicode": "1F4E2", + "digest": "e2d6cf9ec6412ee62f3128a1afd8c63ec74755c4833f01a4f99722407fe154d6" + }, + { + "name": "love_hotel", + "unicode": "1F3E9", + "digest": "184670ebc4045043a7b18d576da3255d216551da522a11cde7df34524e9c7d50" + }, + { + "name": "love_letter", + "unicode": "1F48C", + "digest": "9a4c52e2622fc7d364995ebc93ca530d972134621d117b72053a659dffc90ffc" + }, + { + "name": "low_brightness", + "unicode": "1F505", + "digest": "c177b7fa9fdbef959cc47e7d16becd71117470b767a81ed6d15f80f464776c02" + }, + { + "name": "m", + "unicode": "24C2", + "digest": "2eaf011e74d69613923dad424daaec4c13b592388dbcc5757b645bc058eedecb" + }, + { + "name": "mag", + "unicode": "1F50D", + "digest": "029427bd73d2c79fffc5194ded01f6011952ec0124b7634c6230e0afa7ad7c95" + }, + { + "name": "mag_right", + "unicode": "1F50E", + "digest": "f99de50bb59ec3bf1d4ccb8584ca09d4a7ceb5bf9f600ea8d3f84930efbf01b8" + }, + { + "name": "mahjong", + "unicode": "1F004", + "digest": "da5d1fa980c38e092d414516161ca26046aa65ace3261999ea750f72e676ac6e" + }, + { + "name": "mailbox", + "unicode": "1F4EB", + "digest": "14217df8f39a95fc0a0c527f97db1ca8564764034e921614decc5be705629352" + }, + { + "name": "mailbox_closed", + "unicode": "1F4EA", + "digest": "e0c7beb205ec548a66d8afc7f103b64c6c79c08417ab550f19c36cc6d1a62bc4" + }, + { + "name": "mailbox_with_mail", + "unicode": "1F4EC", + "digest": "6d381c0c4181be628d9409df1d85f8a9438c21ef5b92d82ef8ae1ff0079236de" + }, + { + "name": "mailbox_with_no_mail", + "unicode": "1F4ED", + "digest": "74843d5ea9e03b48323f2252bdd000585f549b7fffe1fe181a25c38b99b5e23d" + }, + { + "name": "man", + "unicode": "1F468", + "digest": "0275935258b4c832c3fcb06531d3e6972e2c3d46bab2973004750a9f00bd4cb6" + }, + { + "name": "man_tone1", + "unicode": "1F468-1F3FB", + "digest": "1f6603d040f4a025f49d384170dd16b8da169663fc3282af1dc8710d9c1a7adf" + }, + { + "name": "man_tone2", + "unicode": "1F468-1F3FC", + "digest": "d65bb03071b483946c69c61769d19b29a2af76fa7e43020e55f0bbc046492221" + }, + { + "name": "man_tone3", + "unicode": "1F468-1F3FD", + "digest": "9af8ede7211b19a7dc0c60db083dd2bdc4897dda4d71e57feadf2e39d847f060" + }, + { + "name": "man_tone4", + "unicode": "1F468-1F3FE", + "digest": "6555de60976aafeb024db78addb44eab2a412dd7277013f44d06757d03b6a252" + }, + { + "name": "man_tone5", + "unicode": "1F468-1F3FF", + "digest": "b58b97a28a6adc1777acc05194cd917c730f90e37441124c384ded12e9a7d2a4" + }, + { + "name": "man_with_gua_pi_mao", + "unicode": "1F472", + "digest": "88663173a6ccbebec5e24883c90d965447e022c6688773273110fe544d5b1607" + }, + { + "name": "man_with_gua_pi_mao_tone1", + "unicode": "1F472-1F3FB", + "digest": "3c8bad3923a619f888e14544d357499a26a517e8fbe7a51027117b960c9eb842" + }, + { + "name": "man_with_gua_pi_mao_tone2", + "unicode": "1F472-1F3FC", + "digest": "da125a3310fab19c9282497d53e2fc71ad07920ce60a0ef52dcdb31500023f09" + }, + { + "name": "man_with_gua_pi_mao_tone3", + "unicode": "1F472-1F3FD", + "digest": "1d5842558847367966bf3ea473ff80fe744359bc5d969f4cc06cf2e452ed2fb6" + }, + { + "name": "man_with_gua_pi_mao_tone4", + "unicode": "1F472-1F3FE", + "digest": "92be490f3ba602a43e2be8160d8bfd8a0691b2f81fe017b06df10f476a89ffab" + }, + { + "name": "man_with_gua_pi_mao_tone5", + "unicode": "1F472-1F3FF", + "digest": "669f6b31bc7a8bf50b169d0600f14e00addaeb24144a1bace8b94950372839b0" + }, + { + "name": "man_with_turban", + "unicode": "1F473", + "digest": "87d30d35ba40ee39c2df8ce19d975ce34a9c54688bafeac7377d7d481e55f1a4" + }, + { + "name": "man_with_turban_tone1", + "unicode": "1F473-1F3FB", + "digest": "33b8b8154e0691e2ad66177dbf1e0101411fd8b3a16bf4e54c36d4a874f2a275" + }, + { + "name": "man_with_turban_tone2", + "unicode": "1F473-1F3FC", + "digest": "1a6b83faa8d6e6a7d12a04898a6f22243287330a1faa081d2626b17dfb07174d" + }, + { + "name": "man_with_turban_tone3", + "unicode": "1F473-1F3FD", + "digest": "5d43da5109e688ff8ca0675f33ebbaf930e206f1f01e3ee773f2844663fe572b" + }, + { + "name": "man_with_turban_tone4", + "unicode": "1F473-1F3FE", + "digest": "bfaf7293c5ea75d0ecdc6fe5afe8f48e7b29b2e0df06ef974d3e1732f5db5dd4" + }, + { + "name": "man_with_turban_tone5", + "unicode": "1F473-1F3FF", + "digest": "fba2404dd3d7eab5268519894cc0b386e1b17fdf14a04760c346014aa0e25acd" + }, + { + "name": "mans_shoe", + "unicode": "1F45E", + "digest": "45dc13ac44c922b4c4b8ecb2e1a870a78e09d53da86843431ab0e9ec96ebcd97" + }, + { + "name": "map", + "unicode": "1F5FA", + "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" + }, + { + "name": "maple_leaf", + "unicode": "1F341", + "digest": "40c5ee93396301911391cf6e70454b6fa8020fe5c85d3364136bcedb5d052cdb" + }, + { + "name": "mask", + "unicode": "1F637", + "digest": "e0301cd27eb8c74c9772ff05b880215fc031ac1ae7f3177cd24ba0acb43b3834" + }, + { + "name": "massage", + "unicode": "1F486", + "digest": "856d0fb1144ee91c58dfad74f9a2cababf6bae4b3ceba2a95c03ecd44ae3aa21" + }, + { + "name": "massage_tone1", + "unicode": "1F486-1F3FB", + "digest": "fd53b06eb0967303c0914ebb79fd872900ec0f71b2852c7238517e192e5023e1" + }, + { + "name": "massage_tone2", + "unicode": "1F486-1F3FC", + "digest": "7ef57359a339ae1ca4488f9a6195a352e74daf5b67d8e1ae1e91fe866921c40c" + }, + { + "name": "massage_tone3", + "unicode": "1F486-1F3FD", + "digest": "e4fb643b6242bedb395e503ae337a88b2a255b5fda88b4aaa93396f948614a6e" + }, + { + "name": "massage_tone4", + "unicode": "1F486-1F3FE", + "digest": "94f007c2daf9455fa8d2b10cc7ccff7db9bc9daf835ef5c3699be091938db833" + }, + { + "name": "massage_tone5", + "unicode": "1F486-1F3FF", + "digest": "d18e800b728bf45b500f492062dc81312ca1ad7b1a0277a3d5bc150e4632ea1c" + }, + { + "name": "meat_on_bone", + "unicode": "1F356", + "digest": "674a2a58e174b7681eef3b6c5b39c098ed9374cc610d037166c0092ee5269a97" + }, + { + "name": "medal", + "unicode": "1F3C5", + "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" + }, + { + "name": "mega", + "unicode": "1F4E3", + "digest": "540ab4fd5bab041a681749b85e6de598ebcbfc4fbf5c3cdbd9ca1e8256191733" + }, + { + "name": "melon", + "unicode": "1F348", + "digest": "39dd0ecb23e2d3da6cbb7309333fed5d7e2cb38c0afc526ade78520eca11b5f4" + }, + { + "name": "menorah", + "unicode": "1F54E", + "digest": "5f81bc2e5a34bf76481d2958fdb0b4e4540c599aa837a6453609a39023885d8c" + }, + { + "name": "mens", + "unicode": "1F6B9", + "digest": "5ed56cff80e8ee7ed581f2a2e365915db5cb29df89e850e0add0b68db4b0c788" + }, + { + "name": "metal", + "unicode": "1F918", + "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" + }, + { + "name": "metal_tone1", + "unicode": "1F918-1F3FB", + "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" + }, + { + "name": "metal_tone2", + "unicode": "1F918-1F3FC", + "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" + }, + { + "name": "metal_tone3", + "unicode": "1F918-1F3FD", + "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" + }, + { + "name": "metal_tone4", + "unicode": "1F918-1F3FE", + "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" + }, + { + "name": "metal_tone5", + "unicode": "1F918-1F3FF", + "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" + }, + { + "name": "metro", + "unicode": "1F687", + "digest": "532378cf385f9a7fafe2f5c8203e675be6d38798871f4c8e2c50498a1529f956" + }, + { + "name": "microphone", + "unicode": "1F3A4", + "digest": "46da2b94e4dc233f640249103f09ec915aaa812cce90afe68fedb6774a27ad4b" + }, + { + "name": "microphone2", + "unicode": "1F399", + "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" + }, + { + "name": "microscope", + "unicode": "1F52C", + "digest": "79918f5fe0a39f31f270a481f4c6e00ea49fc09d64b1ae78770971293c2b1ed8" + }, + { + "name": "middle_finger", + "unicode": "1F595", + "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" + }, + { + "name": "middle_finger_tone1", + "unicode": "1F595-1F3FB", + "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" + }, + { + "name": "middle_finger_tone2", + "unicode": "1F595-1F3FC", + "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" + }, + { + "name": "middle_finger_tone3", + "unicode": "1F595-1F3FD", + "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" + }, + { + "name": "middle_finger_tone4", + "unicode": "1F595-1F3FE", + "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" + }, + { + "name": "middle_finger_tone5", + "unicode": "1F595-1F3FF", + "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" + }, + { + "name": "military_medal", + "unicode": "1F396", + "digest": "bd1da0004768f404c6bb4db85d4b748f766a77ab3edb74e709d0c0064509a043" + }, + { + "name": "milky_way", + "unicode": "1F30C", + "digest": "598b4e641c1081bb03ce38a29f9711fc8616373216a833e4daa14fbe97a358f5" + }, + { + "name": "minibus", + "unicode": "1F690", + "digest": "3d15791ca96349c3abb5bd5d1014b6b33b984db19609f56f5fd1e8d2fc551809" + }, + { + "name": "minidisc", + "unicode": "1F4BD", + "digest": "83c4bfda4e0a80785fa1c3f2bbf3c15aca2bda8ea3727ce78bc4236e1e377a36" + }, + { + "name": "mobile_phone_off", + "unicode": "1F4F4", + "digest": "cfe6dfd766b9e0b4768df25d6e943c9abc0e910ff5e5c7a8a0f425c786bbab8d" + }, + { + "name": "money_mouth", + "unicode": "1F911", + "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" + }, + { + "name": "money_with_wings", + "unicode": "1F4B8", + "digest": "f7f1fa502d2f6804169869aeb5ca7f0ea64bc2d6a0204f08875d65da4f8cb332" + }, + { + "name": "moneybag", + "unicode": "1F4B0", + "digest": "442db49cda27360d2eb781489c9879730a6094c3267bb0a0a8687d84f8fed078" + }, + { + "name": "monkey", + "unicode": "1F412", + "digest": "3141c971aacbadaba21f970a515e192740212be2a49fa1f5eb0fc4dc576e209f" + }, + { + "name": "monkey_face", + "unicode": "1F435", + "digest": "e2397431d2befe44bf5298fa81d865d80722bf954113bceacc2aa98b84d856e2" + }, + { + "name": "monorail", + "unicode": "1F69D", + "digest": "b546153200d6fbe8d65b1b34f62ff4a19b1b6a159eb1b536c5c2ecb56dab0ec9" + }, + { + "name": "mood_bubble", + "unicode": "1F5F0", + "digest": "1df7061217e478d43ab9a87d4f351c4ca56705acd6b4e0b0bedfdece77635f1b" + }, + { + "name": "mood_bubble_lightning", + "unicode": "1F5F1", + "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" + }, + { + "name": "mood_lightning", + "unicode": "1F5F2", + "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" + }, + { + "name": "mortar_board", + "unicode": "1F393", + "digest": "cb59edb08f75c374088b65284e4d0f77b9bc9573de3e6a5127f865431011e54c" + }, + { + "name": "mosque", + "unicode": "1F54C", + "digest": "a08ddb74342dea8f79063db6f98ba03eb08fe99481de8ce9123827ca7f17c7f3" + }, + { + "name": "motorboat", + "unicode": "1F6E5", + "digest": "9dbea67bbe2e95dcc68c049a58f87390a44350b32308342615d75214af3d1cef" + }, + { + "name": "motorcycle", + "unicode": "1F3CD", + "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" + }, + { + "name": "motorway", + "unicode": "1F6E3", + "digest": "fc05a36c917637c135b0a60db8afcd58cee2b335070fe3888697f8026c9d11a5" + }, + { + "name": "mount_fuji", + "unicode": "1F5FB", + "digest": "22bfffef033637b3c9b2fe7e539c74a659d2a49e594d2b33be894da00654d059" + }, + { + "name": "mountain", + "unicode": "26F0", + "digest": "486cf4e9d5f3913d138fdb7878fe869b39caa3fca53876365957a89dc8f7edb8" + }, + { + "name": "mountain_bicyclist", + "unicode": "1F6B5", + "digest": "b547b96951b6837df8ae3be1e846f15e7e2ac06d976e1fe7f1442dcc5d3a0942" + }, + { + "name": "mountain_bicyclist_tone1", + "unicode": "1F6B5-1F3FB", + "digest": "68ce0d55163c7b89ee1d87b752ece127bb25ca9deb3421b31df549a00ac5f69d" + }, + { + "name": "mountain_bicyclist_tone2", + "unicode": "1F6B5-1F3FC", + "digest": "5bfa82180bfb8bc4444cf301688aff02884895574a7ba66b398aaf20bde0f101" + }, + { + "name": "mountain_bicyclist_tone3", + "unicode": "1F6B5-1F3FD", + "digest": "33cb64a792123b81a05080465a0ea1035a2cdfdab01c71f5f725a5f92251c3e8" + }, + { + "name": "mountain_bicyclist_tone4", + "unicode": "1F6B5-1F3FE", + "digest": "9c3fa4e65dcb0ad69b963292e77c7a75853ae3c1d18a90670f81ffb65b5d020c" + }, + { + "name": "mountain_bicyclist_tone5", + "unicode": "1F6B5-1F3FF", + "digest": "871de9e3fddb49b305e5f91000143878b0288c107a125c4e60acf2b6cf8b7f3f" + }, + { + "name": "mountain_cableway", + "unicode": "1F6A0", + "digest": "f248ed5bf864f4a81e365b30d2825d2e6fc15a200c4ccf69e9f797341529f955" + }, + { + "name": "mountain_railway", + "unicode": "1F69E", + "digest": "7dd08745ab56c95c3dfcebcca517ff231cef61b670cedf9d7c53f3244c34e30b" + }, + { + "name": "mountain_snow", + "unicode": "1F3D4", + "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" + }, + { + "name": "mouse", + "unicode": "1F42D", + "digest": "fb20b3a82f407a6316bbbac68d58018c3d5b93a9a6ae968f44ace18d1c5698d9" + }, + { + "name": "mouse2", + "unicode": "1F401", + "digest": "87be4099523ec32440e6d091f1193a8ed90730b9fbecaafed4912585bfe7818c" + }, + { + "name": "mouse_one", + "unicode": "1F5AF", + "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" + }, + { + "name": "mouse_three_button", + "unicode": "1F5B1", + "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" + }, + { + "name": "movie_camera", + "unicode": "1F3A5", + "digest": "d6633b89a637b64d617c3032eed74bb82d3fa732dd9975486b2b5841b473808a" + }, + { + "name": "moyai", + "unicode": "1F5FF", + "digest": "bf948c26cd98e2f5e48da363f2924a9d7c217232115a00cec372d0d5293402a8" + }, + { + "name": "muscle", + "unicode": "1F4AA", + "digest": "c85147efb786bdea3e7d53e2edf6b827280cd9fa881661a6102a614bf5b3579f" + }, + { + "name": "muscle_tone1", + "unicode": "1F4AA-1F3FB", + "digest": "38d071df2b25031b61f3605b03c34d2e5d3e35d29f3c4aada14be37e19750eb8" + }, + { + "name": "muscle_tone2", + "unicode": "1F4AA-1F3FC", + "digest": "dcf11b76c8ffb58dc7e4f9ecd32a4c291d9772d51df2853d41081e041e7e0876" + }, + { + "name": "muscle_tone3", + "unicode": "1F4AA-1F3FD", + "digest": "a3d5f8f2dbfc28f9713ee657428ea3292c47d0b22f11a51c13594be22b0f5204" + }, + { + "name": "muscle_tone4", + "unicode": "1F4AA-1F3FE", + "digest": "eb220fc19be58d16cacc6b721e1011078b03256c0245756f251a4c2bcf50586c" + }, + { + "name": "muscle_tone5", + "unicode": "1F4AA-1F3FF", + "digest": "4e18708cbd61eaad288f913c86ad2d45108dd4484bc35879c5dcdd075eeb09fd" + }, + { + "name": "mushroom", + "unicode": "1F344", + "digest": "a2b252cd759244409d9a8066470059948e2c50b8cc86b59821c1c86b5190f640" + }, + { + "name": "musical_keyboard", + "unicode": "1F3B9", + "digest": "dcb3e84d27bfe373e5ea7ede457908de52002f0fd6105e9f3f5525c54d2a43dd" + }, + { + "name": "musical_note", + "unicode": "1F3B5", + "digest": "76a0f598f8e251a9dab44f2e14f2b7a6fb0c0c351e0f37862c8c99d380f1c261" + }, + { + "name": "musical_score", + "unicode": "1F3BC", + "digest": "a132c6b35236005b45c830a42fa97b454d3061c14991c6320f34807f10ba6a4a" + }, + { + "name": "mute", + "unicode": "1F507", + "digest": "73a99b7f9e00f92cab78cd304dee4e893a112c3a6f2285c13d44916ea547458e" + }, + { + "name": "nail_care", + "unicode": "1F485", + "digest": "62f721d3610d1647dba4b3f53cd4f2bc4180dae298314c2cca2a6a8ab1664525" + }, + { + "name": "nail_care_tone1", + "unicode": "1F485-1F3FB", + "digest": "11b82ed2e6b6619c9b74702fdacfb0ddc91310191c8b89f355c7c69a72673f8f" + }, + { + "name": "nail_care_tone2", + "unicode": "1F485-1F3FC", + "digest": "5195c76bccb9149d9080347d785dae2cce947bada5b198fae8c23e42f5553154" + }, + { + "name": "nail_care_tone3", + "unicode": "1F485-1F3FD", + "digest": "50eab0bf825c5e00db07a3f5ad26b1bb221f54efb5c55549f392b2f5aec09e5a" + }, + { + "name": "nail_care_tone4", + "unicode": "1F485-1F3FE", + "digest": "d05a9ccfad02191c89e4cbd00aa48fdaf908c0de6681f4a587d500be448e528f" + }, + { + "name": "nail_care_tone5", + "unicode": "1F485-1F3FF", + "digest": "62466354dcf6717a8b9e942ca2c5ad15a26aa815c213e3b01faba9a2e302ecdd" + }, + { + "name": "name_badge", + "unicode": "1F4DB", + "digest": "0a1cb0f7d489d3356a4d3e01f9faf78449d82d8ec4595c8639a55c3606c97c40" + }, + { + "name": "necktie", + "unicode": "1F454", + "digest": "029e1140391ef559a9316021c2db94f05653751fdf9d8f366446467a70fee6df" + }, + { + "name": "negative_squared_cross_mark", + "unicode": "274E", + "digest": "0ba0e705fdeac99edd712db31a8846320b9d2cf53c9cb4d4bcfd22ba4e1488ea" + }, + { + "name": "nerd", + "unicode": "1F913", + "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" + }, + { + "name": "network", + "unicode": "1F5A7", + "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" + }, + { + "name": "neutral_face", + "unicode": "1F610", + "digest": "df01da8501e1f588049c8ed66e504e9abcce83f74ce5790f4d3dc547408f77ee" + }, + { + "name": "new", + "unicode": "1F195", + "digest": "24e80abd29750d8b297335cdd4751b6250bb820560cf0392a6cc8783d34db63a" + }, + { + "name": "new_moon", + "unicode": "1F311", + "digest": "2d697e431eac53d6e1ea367b5da03c15fc535cd7e8c214f801fe595b768a8e11" + }, + { + "name": "new_moon_with_face", + "unicode": "1F31A", + "digest": "ea469a4668ded071f35e5898ae229fdb5d02b0730ce233169b83e22f81292baa" + }, + { + "name": "newspaper", + "unicode": "1F4F0", + "digest": "0aaf6747a43fb60cd15e6e64ca0eccaade331b376c6fe6712fd5e8294e9868cc" + }, + { + "name": "newspaper2", + "unicode": "1F5DE", + "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" + }, + { + "name": "ng", + "unicode": "1F196", + "digest": "4994c9b795033ed788e98c4af571a1dffe28c0a1479e3b42dcae21bb08381b5f" + }, + { + "name": "night_with_stars", + "unicode": "1F303", + "digest": "56bb4a59a897c1836ee1a49cc99f468891b790b0f8bce203c201c13bb7b8ae9a" + }, + { + "name": "nine", + "unicode": "0039-20E3", + "digest": "7e3644a98cb6417a351530c9ce6b368e637a22c847a8c04133897dc1c5d7419f" + }, + { + "name": "no_bell", + "unicode": "1F515", + "digest": "f4fb42836132000101624fecef8b9358736a0fc76beae460e6986aaa479204fd" + }, + { + "name": "no_bicycles", + "unicode": "1F6B3", + "digest": "b3c258bea7d6988640e3348598c03c97632ca00a11cbf0352995b801ff4a296b" + }, + { + "name": "no_entry", + "unicode": "26D4", + "digest": "ac807d54092efdc3aea417790a7d0c50b59800c9ea49b37f1aec6d2e453c5f6d" + }, + { + "name": "no_entry_sign", + "unicode": "1F6AB", + "digest": "5a17d677ec1c7595a7970a1cbe0d20909341b30d3ab31471ced590f51fff1ff7" + }, + { + "name": "no_good", + "unicode": "1F645", + "digest": "8ce921e5e13e1203cf43fdc3e7c5ec1fb2a1f9ff79f21539cff542c80af2e5fe" + }, + { + "name": "no_good_tone1", + "unicode": "1F645-1F3FB", + "digest": "aab4d354aaac06e8348eb354487c6381e475b44651cb2716660904a36c47a1b6" + }, + { + "name": "no_good_tone2", + "unicode": "1F645-1F3FC", + "digest": "8fb66b1a7b8f72062794281294515d47471a8c59de300b99d656c3412ca19d64" + }, + { + "name": "no_good_tone3", + "unicode": "1F645-1F3FD", + "digest": "aeecf73fb9dca24b4002db2802fc9b5a483644c49f834c19f143d4e56ec46c1a" + }, + { + "name": "no_good_tone4", + "unicode": "1F645-1F3FE", + "digest": "fadeb23307d5ccabbf08c848cf81c66c05b152aa32b85f86061caf14760f8eb9" + }, + { + "name": "no_good_tone5", + "unicode": "1F645-1F3FF", + "digest": "cf26d5d6463d0febf4e1f08e343308742ffe0811cfc30c459b87d4cc812f5d04" + }, + { + "name": "no_mobile_phones", + "unicode": "1F4F5", + "digest": "3b4ead88beca33f1e303d0a45268849be7aaaff7830b761732c7a5afc5a2de3a" + }, + { + "name": "no_mouth", + "unicode": "1F636", + "digest": "2af81a3e07a8b7827a1e58f6f5036ccff2f6e7b0027a4f934c9fa34c6a780963" + }, + { + "name": "no_pedestrians", + "unicode": "1F6B7", + "digest": "9f9ed90bb8f9964fa8cb0048fc092ecc0612a1994c98d19ef0b5a58607888476" + }, + { + "name": "no_smoking", + "unicode": "1F6AD", + "digest": "fb90290ff5c917b7307a97c8ba793d20c61042525cf2f7bfd4cd2a7878aeefa5" + }, + { + "name": "non-potable_water", + "unicode": "1F6B1", + "digest": "c4ddca2ab1a97260e9b2c2aa33fb03455c0e8174541c3a9416fc44143a3ee567" + }, + { + "name": "nose", + "unicode": "1F443", + "digest": "308e28b15b7f734f6f184ae367789d7cf258656b24861cf8d5935127d810aa3f" + }, + { + "name": "nose_tone1", + "unicode": "1F443-1F3FB", + "digest": "392d24b38ac3edc2d7b83945a60bbe9115a6a97658d8af35281a7cbef79449e8" + }, + { + "name": "nose_tone2", + "unicode": "1F443-1F3FC", + "digest": "409a790339c405770492e49fdb0b5ba34087c27e2f9018ecd845ab078e61476a" + }, + { + "name": "nose_tone3", + "unicode": "1F443-1F3FD", + "digest": "92b52b479a935f31e460257d809c531edad1a6bb4583ad18233b12c4e45202fe" + }, + { + "name": "nose_tone4", + "unicode": "1F443-1F3FE", + "digest": "78ad2e857792e86cded6ba5620f634f7d1f79a92c82c266e48fab9bd73df3688" + }, + { + "name": "nose_tone5", + "unicode": "1F443-1F3FF", + "digest": "dbef6813c1965d3e93f70f33f118f9950130af21c622cea97ea215a36b4fa73f" + }, + { + "name": "note", + "unicode": "1F5C9", + "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" + }, + { + "name": "note_empty", + "unicode": "1F5C6", + "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" + }, + { + "name": "notebook", + "unicode": "1F4D3", + "digest": "64bd4a3e7ca7b22fc704c7b7bd4d13540c16bc69b9d8dd76e69e6ad573ab3823" + }, + { + "name": "notebook_with_decorative_cover", + "unicode": "1F4D4", + "digest": "4b45f28fbde1be5c214a6bc2413abc91db02bccd86f74c21b7f4a4da8b75a46f" + }, + { + "name": "notepad", + "unicode": "1F5CA", + "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" + }, + { + "name": "notepad_empty", + "unicode": "1F5C7", + "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" + }, + { + "name": "notepad_spiral", + "unicode": "1F5D2", + "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" + }, + { + "name": "notes", + "unicode": "1F3B6", + "digest": "bf3868386e17eac40ac7fbabea027042027ff061daafe406c869cdd8ce94641d" + }, + { + "name": "nut_and_bolt", + "unicode": "1F529", + "digest": "fdb9d7408202fad7a52ff21608042c08c3b0beb195999fff233df36a29dc9e96" + }, + { + "name": "o", + "unicode": "2B55", + "digest": "8e119dba4130bd33b3ee5c862fb4fa5a691173911ffee51cb9359fee3398e330" + }, + { + "name": "o2", + "unicode": "1F17E", + "digest": "00d751124c25633611055bd61e74fc3f3d1779f0d09e1e707837686f613367b4" + }, + { + "name": "ocean", + "unicode": "1F30A", + "digest": "9b1fbfd2a64f417d0c2cb91085b29a12d14e15844bc21798bdee938bb7bf6222" + }, + { + "name": "octopus", + "unicode": "1F419", + "digest": "3fdfbc02f47ad434bdeb7f3a15cd4e8f8118ee1cd754627e358f1c2f4616f5e3" + }, + { + "name": "oden", + "unicode": "1F362", + "digest": "afed1c5166943e5803602ffacc67652e3b29ee4222a6c36aba2daf88bd21ad3c" + }, + { + "name": "office", + "unicode": "1F3E2", + "digest": "dc1836ef152d88fd628df18db770594f5dbc8d7f20d6ce982588b25b78b19c92" + }, + { + "name": "oil", + "unicode": "1F6E2", + "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" + }, + { + "name": "ok", + "unicode": "1F197", + "digest": "6b05bbab4a7104541c2f4bce553884d17ae0ad07589b19d6b53b6949c14f2269" + }, + { + "name": "ok_hand", + "unicode": "1F44C", + "digest": "9981f32ef200b011a10f6bfa2066c41b6b5e7bcd6c3c21647980b640bc1fa93b" + }, + { + "name": "ok_hand_tone1", + "unicode": "1F44C-1F3FB", + "digest": "e5933a9b64b03ce0634f15f02ff7b6424530dbdc0e283461e0c9992d0c2ca2ad" + }, + { + "name": "ok_hand_tone2", + "unicode": "1F44C-1F3FC", + "digest": "4c04741c9f2c8731da8df3015e9aae00061a01848c2d22aab1e9853c271deed3" + }, + { + "name": "ok_hand_tone3", + "unicode": "1F44C-1F3FD", + "digest": "216dc5a72f9e34bbb7b39f680c388bd5b52abf9b41b843342e53e285b7933076" + }, + { + "name": "ok_hand_tone4", + "unicode": "1F44C-1F3FE", + "digest": "7139de7ec9d5a962cf87b9fbbeef3a53aa482bb840ab3b64d8d0da81bdc19886" + }, + { + "name": "ok_hand_tone5", + "unicode": "1F44C-1F3FF", + "digest": "e18b0a1bc5d970cc63466bd6da6e9f855db37d1eada3230d19f600c1f5a402a3" + }, + { + "name": "ok_woman", + "unicode": "1F646", + "digest": "3b2fa732d9c9addb056f136192428e99d805d4cb1c7dab724fd552c7e93197e4" + }, + { + "name": "ok_woman_tone1", + "unicode": "1F646-1F3FB", + "digest": "017aca3797701b043a44f22e67dcad8b531a3ca14e629ae0d2fbc601ed3e49cb" + }, + { + "name": "ok_woman_tone2", + "unicode": "1F646-1F3FC", + "digest": "036bed032bc5a616668775cda0d5640c810e2836aa28009c8e8bf2b487259c59" + }, + { + "name": "ok_woman_tone3", + "unicode": "1F646-1F3FD", + "digest": "d9a4414caddda43d1a36828cfbecce5f2b7e5c1b67b4a47991b2ae0a34cf7ab7" + }, + { + "name": "ok_woman_tone4", + "unicode": "1F646-1F3FE", + "digest": "942e1b9aa495c4c4de0804e4d4348422201299d649e5d65829ba4a308880df1c" + }, + { + "name": "ok_woman_tone5", + "unicode": "1F646-1F3FF", + "digest": "e8d0fb5b999d5d63404493aa505b5af2260c76001023431d5e788773d0a9e2de" + }, + { + "name": "older_man", + "unicode": "1F474", + "digest": "620f763325827acbeb9d57798ef55d87827d0dfc77b84d942e25bc5057f2cbfe" + }, + { + "name": "older_man_tone1", + "unicode": "1F474-1F3FB", + "digest": "e0f35c12362eae503d1c30a345c3a4978196d351d8a1eb9d5f107c60ea4bbf52" + }, + { + "name": "older_man_tone2", + "unicode": "1F474-1F3FC", + "digest": "671766ce9fa47c3fa009d4f138344c87d73032a1c38e48614c663f8ea5d0f673" + }, + { + "name": "older_man_tone3", + "unicode": "1F474-1F3FD", + "digest": "6ff4885ef8c416b8970780a691fef74c8d89421ab11e0aa8c522c33e1c67fbe8" + }, + { + "name": "older_man_tone4", + "unicode": "1F474-1F3FE", + "digest": "0ae7d4e316dcd4d27a5a6cdaabab88a4f992bd1b75f6ceaeb5b906ed1eb5269c" + }, + { + "name": "older_man_tone5", + "unicode": "1F474-1F3FF", + "digest": "abe2757bd5e35f30d2a6daec09637ea5382a46d14d239b77282e9bf874229b57" + }, + { + "name": "older_woman", + "unicode": "1F475", + "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" + }, + { + "name": "older_woman_tone1", + "unicode": "1F475-1F3FB", + "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" + }, + { + "name": "older_woman_tone2", + "unicode": "1F475-1F3FC", + "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" + }, + { + "name": "older_woman_tone3", + "unicode": "1F475-1F3FD", + "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" + }, + { + "name": "older_woman_tone4", + "unicode": "1F475-1F3FE", + "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" + }, + { + "name": "older_woman_tone5", + "unicode": "1F475-1F3FF", + "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" + }, + { + "name": "om_symbol", + "unicode": "1F549", + "digest": "c8c1c9d445b1fc50a627b71bee21fba978e04532e4685ec032a0174f51fc12bb" + }, + { + "name": "on", + "unicode": "1F51B", + "digest": "08e1159a68d3334a87ffa75b9e70826cb557d0f73a2c1d08f4c3d60476ecacc8" + }, + { + "name": "oncoming_automobile", + "unicode": "1F698", + "digest": "6bff7f40fe223df6d16c7512532b8aa6f83e8c13e1007b63eb9aabf774c1a322" + }, + { + "name": "oncoming_bus", + "unicode": "1F68D", + "digest": "127a357fcd96ce4b9ab11c3dba95d8ff811bab193dd8ba38efb7067a44752ce8" + }, + { + "name": "oncoming_police_car", + "unicode": "1F694", + "digest": "57cb70e05e70c1f68ab42259f307ed9782c2b9d6e35d2dff2895aa23d7eb6b04" + }, + { + "name": "oncoming_taxi", + "unicode": "1F696", + "digest": "174967ae4c3d5881d2408c71c020f704e933190af4caef5d2908e9ac382f35ea" + }, + { + "name": "one", + "unicode": "0031-20E3", + "digest": "113b9d87c3e37c9c54e49cecccbfc40c15fb97fd03a51505df85e48b78702b2b" + }, + { + "name": "open_file_folder", + "unicode": "1F4C2", + "digest": "def93715203aed464211798d773732895a19389a94a2e7ed43e7f229b2aab7da" + }, + { + "name": "open_hands", + "unicode": "1F450", + "digest": "7c60a37ae11727c998908199b8709e52593b931843aef942f37b306b1edca12a" + }, + { + "name": "open_hands_tone1", + "unicode": "1F450-1F3FB", + "digest": "09ffa9b3f28fc56a71e4e711bdfc87ce1a56721229377e71f1c00224523f8b9b" + }, + { + "name": "open_hands_tone2", + "unicode": "1F450-1F3FC", + "digest": "21ecaba9f086bcb7eb07c17c2b2621bcd1ca28c57f79032d5e0eba356494cc85" + }, + { + "name": "open_hands_tone3", + "unicode": "1F450-1F3FD", + "digest": "c7dbb8c44f78f7793b202ec215fee42b7e1e555d659fbf402383500217b89656" + }, + { + "name": "open_hands_tone4", + "unicode": "1F450-1F3FE", + "digest": "867451d42492ab2277687447f421f744530b9ea057312326353fec39c94b18fd" + }, + { + "name": "open_hands_tone5", + "unicode": "1F450-1F3FF", + "digest": "56335506cf68e29150cb68d7ebbb4a92aed390018966669a8144d20ae0d6cfe3" + }, + { + "name": "open_mouth", + "unicode": "1F62E", + "digest": "f05fdf998e8b5c0b00ebd8b5ab17a67f5c0a45275f31a201af74e8ab0c2f7ba9" + }, + { + "name": "ophiuchus", + "unicode": "26CE", + "digest": "98c61bb0c36d60c476d42d5e074297662e8d141dcab7004a5bd63c359eed3b84" + }, + { + "name": "optical_disk", + "unicode": "1F5B8", + "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" + }, + { + "name": "orange_book", + "unicode": "1F4D9", + "digest": "86d150ea3d62183ab7dfe2851cf7f4d1ae769b7ecbb1987b0f463e639e429598" + }, + { + "name": "orthodox_cross", + "unicode": "2626", + "digest": "9c861285ca6d699cd2c72b6df44ec2b1e64138152f19c66e32df1ce770ff2e83" + }, + { + "name": "outbox_tray", + "unicode": "1F4E4", + "digest": "b6a6015d5d7d528af485de23ff4518dc35408def1cc49bc6c9b01d880d613985" + }, + { + "name": "ox", + "unicode": "1F402", + "digest": "cbcfe5c8c4d6b939e24e18e610785f171bb9410441e02c2eeb1bceb0a6246daf" + }, + { + "name": "package", + "unicode": "1F4E6", + "digest": "4023cffce85384217a73609f457aec013876e689c44bcfff0bcc35f3e4e1ab00" + }, + { + "name": "page", + "unicode": "1F5CF", + "digest": "cc745056525f59d9128d1d03b14770376bb09ab64b8ef4ac994ab7f38efd4783" + }, + { + "name": "page_facing_up", + "unicode": "1F4C4", + "digest": "71a0872bf1b13c58746f9b41655227c75be107ab6083c0dce13cb16444af22e7" + }, + { + "name": "page_with_curl", + "unicode": "1F4C3", + "digest": "cb4210464faea946c7b07db7067c7fc98920f778cf57721388f5362942ba3029" + }, + { + "name": "pager", + "unicode": "1F4DF", + "digest": "209dbdc19aa650ecacc0569e17a9123c9a1e39df59c9b4120f3b0888b63cd6f1" + }, + { + "name": "pages", + "unicode": "1F5D0", + "digest": "05bd47b78f089389356d9d839c736843f56b959ab4277056606ffcbb013390bc" + }, + { + "name": "paintbrush", + "unicode": "1F58C", + "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" + }, + { + "name": "palm_tree", + "unicode": "1F334", + "digest": "1589ff4b1b87296edc0118e4aa67b3b504ed85a5b8d47e7d0c3e309d0bbf8cd6" + }, + { + "name": "panda_face", + "unicode": "1F43C", + "digest": "050ee87892f56ff485f460bc6c3846d98a0ca7083d2cf0b8ab24772b672273f2" + }, + { + "name": "paperclip", + "unicode": "1F4CE", + "digest": "1463607a59345973f009fa53a719e2264b95743560adb99737bef29b1d133a95" + }, + { + "name": "paperclips", + "unicode": "1F587", + "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" + }, + { + "name": "park", + "unicode": "1F3DE", + "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" + }, + { + "name": "parking", + "unicode": "1F17F", + "digest": "e1d2cfd1c57ea85003ca4df066cbba4e506bf6c4d6c790e27b2f78ad8443fabf" + }, + { + "name": "part_alternation_mark", + "unicode": "303D", + "digest": "b3cc2e803b255e858417345ba6ba52a1c22f511b483fec11b5d68c4432f759b6" + }, + { + "name": "partly_sunny", + "unicode": "26C5", + "digest": "484990f5e1a3b14c731e7bd4b0b4a1c10cd5fb54ac7cf2751f40c8bf59d7e2b4" + }, + { + "name": "passport_control", + "unicode": "1F6C2", + "digest": "224e8ef60d4d6587721727555de324948fb5b6c1cb5cc4b546960983d1ec85c4" + }, + { + "name": "pause_button", + "unicode": "23F8", + "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" + }, + { + "name": "peace", + "unicode": "262E", + "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" + }, + { + "name": "peach", + "unicode": "1F351", + "digest": "a3f4fd5ff02e0a03104ab54456ee1a7521858ee68443856ee10e0972e5b6aaa5" + }, + { + "name": "pear", + "unicode": "1F350", + "digest": "7a7a72568d53677cd1fff4d9e58e63327a742fa16d22a2bef03b4a6fa378d3b3" + }, + { + "name": "pen_ballpoint", + "unicode": "1F58A", + "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" + }, + { + "name": "pen_fountain", + "unicode": "1F58B", + "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" + }, + { + "name": "pencil", + "unicode": "1F4DD", + "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" + }, + { + "name": "pencil2", + "unicode": "270F", + "digest": "aa2c572772187fee1f9125bb0950f5ce8a61f7dd2647258c40b4077ee5feb498" + }, + { + "name": "pencil3", + "unicode": "1F589", + "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" + }, + { + "name": "penguin", + "unicode": "1F427", + "digest": "095de34b3f6a2521a342c21f5f2551a0092bf47429801c15b7bbf0913924f412" + }, + { + "name": "pennant_black", + "unicode": "1F3F2", + "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" + }, + { + "name": "pennant_white", + "unicode": "1F3F1", + "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" + }, + { + "name": "pensive", + "unicode": "1F614", + "digest": "2d9e7f1eed14dcc86674cec78e992567a40d0f223fc67d722b91eebcd1251269" + }, + { + "name": "performing_arts", + "unicode": "1F3AD", + "digest": "a202755bab6427433975589bb8b63e61e5d7f55c6242676d8000e91eedabc55e" + }, + { + "name": "persevere", + "unicode": "1F623", + "digest": "686ef3fc70ce8294d02a764ebd75b69f25cca6bff6b92e7905130366d22f6d8a" + }, + { + "name": "person_frowning", + "unicode": "1F64D", + "digest": "16e8fbf22c0b4c237d0d45202fa32d1ebd04760a5b6975c9c9b477321ccb0e12" + }, + { + "name": "person_frowning_tone1", + "unicode": "1F64D-1F3FB", + "digest": "a143b865976ce3cf307db854cfd1ca58c3832df0eee5e9b0ab307cf4f24ba3db" + }, + { + "name": "person_frowning_tone2", + "unicode": "1F64D-1F3FC", + "digest": "4e7050d8a38019ba2293f66b9930e6a7e35dacf3b3bc9431edb586a0d9ea8054" + }, + { + "name": "person_frowning_tone3", + "unicode": "1F64D-1F3FD", + "digest": "0750015d3ac1b5954d31e36cd59c70b6ed9f4df698082484b7ac59eb0b9964b0" + }, + { + "name": "person_frowning_tone4", + "unicode": "1F64D-1F3FE", + "digest": "18d6cc92d0990624218d38d6eeed60bccb371d0fc9f1c889e9476b3b0c44b5e8" + }, + { + "name": "person_frowning_tone5", + "unicode": "1F64D-1F3FF", + "digest": "4a898199cbaf083d37511f51d8a1d2560b7a20c62a1b09087831da7010fbd093" + }, + { + "name": "person_with_blond_hair", + "unicode": "1F471", + "digest": "67d95a0801c65f62db55fa80ab35dec65c239601a44bf5f5902e4645f126770e" + }, + { + "name": "person_with_blond_hair_tone1", + "unicode": "1F471-1F3FB", + "digest": "e79717bfe30a26eafc082a75fa7547d8f2ad3c123fb2d75a95e75f0ce7ecbd0c" + }, + { + "name": "person_with_blond_hair_tone2", + "unicode": "1F471-1F3FC", + "digest": "c4a1961c292149ab6e1fd54a7894398599bf855de97a05ee4e836a86a400deb3" + }, + { + "name": "person_with_blond_hair_tone3", + "unicode": "1F471-1F3FD", + "digest": "e2707d0cf778bee5b72d861ec76430eb1cf9f9820f066ee6327574d5697f445e" + }, + { + "name": "person_with_blond_hair_tone4", + "unicode": "1F471-1F3FE", + "digest": "94da43f0b12ef4a98dabec096ff1184b0a9b5b6ee55824d257e5112cc7e88730" + }, + { + "name": "person_with_blond_hair_tone5", + "unicode": "1F471-1F3FF", + "digest": "9e096a210ea720d32bc6a7005cd77f8b314ccf817fc3060da2e1796de39e9d60" + }, + { + "name": "person_with_pouting_face", + "unicode": "1F64E", + "digest": "8c3199a422250d2db9a163156191ed2c6697d7f31699e2efe19e05ca26e5d225" + }, + { + "name": "person_with_pouting_face_tone1", + "unicode": "1F64E-1F3FB", + "digest": "3e1f09bbf607381c992739ea92dd35cbd26b1bbc705a7d21b7c3156f50e9d8b3" + }, + { + "name": "person_with_pouting_face_tone2", + "unicode": "1F64E-1F3FC", + "digest": "b5fc1cf3fdc5ff01105ee2452db90baa6a52c1e42f3795b2836c3e35197ece1f" + }, + { + "name": "person_with_pouting_face_tone3", + "unicode": "1F64E-1F3FD", + "digest": "e8ec2539c458a8283c8c1050634c432b6363f3e64b68ba4c977994782f09b564" + }, + { + "name": "person_with_pouting_face_tone4", + "unicode": "1F64E-1F3FE", + "digest": "5cab7a29699decd45682583446c2bf56ddcd69cd16e14db661b526a4076dfa17" + }, + { + "name": "person_with_pouting_face_tone5", + "unicode": "1F64E-1F3FF", + "digest": "3caebd3626fd77d849859d1c99a747f80a2b59bfa5c1854494f1ce0485539a94" + }, + { + "name": "pick", + "unicode": "26CF", + "digest": "24a3e8f592435b97272e6d134ea5503dce3012811659c4aadbad4e45d9fba679" + }, + { + "name": "pig", + "unicode": "1F437", + "digest": "50b55fc74e8f6c89c6e04609381c99a660748908f0ef015f5da37089678ad0c3" + }, + { + "name": "pig2", + "unicode": "1F416", + "digest": "e8189fb678608e8b9d69e11d2566f9a4765cbdff99ec8e66df30c7a2dabf742f" + }, + { + "name": "pig_nose", + "unicode": "1F43D", + "digest": "7e299cb49a771884f5065c68733a5a1fe354a54cff009127230177f1717af4a5" + }, + { + "name": "pill", + "unicode": "1F48A", + "digest": "53ae3379cc6721744979122569f157a5a13aa6b48e081a89f17b2d90134efe9e" + }, + { + "name": "pineapple", + "unicode": "1F34D", + "digest": "ceda8ffa4a41594f28a4e69d03f8a1daeb2ba20740f0b8c56447cae833eea035" + }, + { + "name": "ping_pong", + "unicode": "1F3D3", + "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" + }, + { + "name": "piracy", + "unicode": "1F572", + "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" + }, + { + "name": "pisces", + "unicode": "2653", + "digest": "75f11b9a094196b54a242420362fa7c0aeba7cfc497b187e1aaaba96d93684a7" + }, + { + "name": "pizza", + "unicode": "1F355", + "digest": "ac94ae1c034f7b854ce2a483e1c219d101a84336f5065342f4824ff32ba705c4" + }, + { + "name": "place_of_worship", + "unicode": "1F6D0", + "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" + }, + { + "name": "play_pause", + "unicode": "23EF", + "digest": "d69e8cdec33447283cf65d343b986115e27681d781b721db7894e5c587ca18ad" + }, + { + "name": "point_down", + "unicode": "1F447", + "digest": "685f46a643be7f3033896e59a822f87d61ce50db6969bcdbacc743215a96bb7a" + }, + { + "name": "point_down_tone1", + "unicode": "1F447-1F3FB", + "digest": "d3dd2608fe17d5649c960fcf8dbdb68466908d80fa349b7947b457da2a27ebb1" + }, + { + "name": "point_down_tone2", + "unicode": "1F447-1F3FC", + "digest": "67ab236a14f6d63abcdb26433a66a183d223186c21ebc9f978fab50165ebe271" + }, + { + "name": "point_down_tone3", + "unicode": "1F447-1F3FD", + "digest": "c8a2368f2cedb5bbb5cc0195b97fbf3787747637bf6e77bdc9a4edf4a3f22a04" + }, + { + "name": "point_down_tone4", + "unicode": "1F447-1F3FE", + "digest": "6a92eab3bc8f950fa423e690f54a352887bda92f01e91c62eb3f3a9544c10cd8" + }, + { + "name": "point_down_tone5", + "unicode": "1F447-1F3FF", + "digest": "6ad329f156414f421d6f8cf5e2a68d34b7a41f90d80e8e66b15bcbd3788126c7" + }, + { + "name": "point_left", + "unicode": "1F448", + "digest": "cb520d6bba4c2b3bd7911315c9efce3261d048ff090437d7e24c9c5a7255043e" + }, + { + "name": "point_left_tone1", + "unicode": "1F448-1F3FB", + "digest": "81813901bdaa8d261277f79aff9e9a21beb80a5855899941820b25f70786ec21" + }, + { + "name": "point_left_tone2", + "unicode": "1F448-1F3FC", + "digest": "ecdc3dea0d644290aa7e0dab758c215822482a482ba35d825a33152453593c1e" + }, + { + "name": "point_left_tone3", + "unicode": "1F448-1F3FD", + "digest": "84e73b6a37755016271c255eba164f349dbd2a2badf5d9ac1c6f4cbfcae589f0" + }, + { + "name": "point_left_tone4", + "unicode": "1F448-1F3FE", + "digest": "d16800499b6c6ede94256796b1de8a8f723879f636849856b3bd8b7a092b5576" + }, + { + "name": "point_left_tone5", + "unicode": "1F448-1F3FF", + "digest": "18b7108066cebf2d4090f29e595a2f01db94bd210f3b1d61dc269ec249a749b9" + }, + { + "name": "point_right", + "unicode": "1F449", + "digest": "866180bf31e92de32aba336d5b5ce81773a29cdaadada1d93c944cf9ad9783bc" + }, + { + "name": "point_right_tone1", + "unicode": "1F449-1F3FB", + "digest": "ebe2e4bf6bd46a5798b9a845a4ed055911c4fe58dbeacc4d39d6ea63e28e7cc9" + }, + { + "name": "point_right_tone2", + "unicode": "1F449-1F3FC", + "digest": "b638662a67b1c6adde4f5abc789aae010b178404cdd1b71fcc982cdf8307c655" + }, + { + "name": "point_right_tone3", + "unicode": "1F449-1F3FD", + "digest": "32c6ca2f992416ab2c36672dfbc1c0de8f102c77a13496dd8d63736a7b0261d2" + }, + { + "name": "point_right_tone4", + "unicode": "1F449-1F3FE", + "digest": "89bd6828e9b82408a3829d49fa43332e2599f7d10bc6e5b14b750ef03267b173" + }, + { + "name": "point_right_tone5", + "unicode": "1F449-1F3FF", + "digest": "390525048a12b0efa22de550c800e439b0deaad03f1f31155d179aef093354af" + }, + { + "name": "point_up", + "unicode": "261D", + "digest": "31b5ca1303c1afabe1db322b24f73b23f3568c87a364f61c82f6e0c858c090e9" + }, + { + "name": "point_up_2", + "unicode": "1F446", + "digest": "55c237054aa347c9847f3f3f577eb755db55dfcf793aa7de0f8f868574d70e8f" + }, + { + "name": "point_up_2_tone1", + "unicode": "1F446-1F3FB", + "digest": "dc07e7732d973de96ae3b08b14c19e20b6c1aea7f5a30e7198679b750422e914" + }, + { + "name": "point_up_2_tone2", + "unicode": "1F446-1F3FC", + "digest": "af2211fc4a1bd51d1e76f7bc43a6fa87bdd24e4295c52fdbdb01c1ca670a6cd7" + }, + { + "name": "point_up_2_tone3", + "unicode": "1F446-1F3FD", + "digest": "917701169b3fb3e1b6e14a68e9572b25998ef2e38abac9ad8cf30100f8ea0dac" + }, + { + "name": "point_up_2_tone4", + "unicode": "1F446-1F3FE", + "digest": "20843904764c6c3e55792cce0c55c76f72b97788c5229cad655ebf1f2873b439" + }, + { + "name": "point_up_2_tone5", + "unicode": "1F446-1F3FF", + "digest": "1d0cca546027c717da50f90da65757af46fe7cd4e397da9b8e203446f707208d" + }, + { + "name": "point_up_tone1", + "unicode": "261D-1F3FB", + "digest": "5ede60379dee23166c6b834d73da8b55268e330f67058843b8a3705dca6ed71a" + }, + { + "name": "point_up_tone2", + "unicode": "261D-1F3FC", + "digest": "c94a15ef848d410aa5d32b8d0e453b59682fde6f39e6705cbb81cf0829833a81" + }, + { + "name": "point_up_tone3", + "unicode": "261D-1F3FD", + "digest": "d319ce72876d97a3b1d4bc7c0679e546a983f02145d723a0da5ed0b73a51cfe7" + }, + { + "name": "point_up_tone4", + "unicode": "261D-1F3FE", + "digest": "9171a27f86f27fd144347a17153fb56e30bd32e67a8f10f8c1f32a40cad4e009" + }, + { + "name": "point_up_tone5", + "unicode": "261D-1F3FF", + "digest": "a894f87da4c3d33d5e6e74d003a33ec60c453db6507fe05d22235f807ead27d6" + }, + { + "name": "police_car", + "unicode": "1F693", + "digest": "7999869cb75be404fc34942b6f9d8e84fa7e259aa892a1e8e1652a5f02cceea6" + }, + { + "name": "poodle", + "unicode": "1F429", + "digest": "8a568d8688bf19b440b7c1b49fcfe6672b8f75af0031d89ab6212623430acadb" + }, + { + "name": "poop", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "popcorn", + "unicode": "1F37F", + "digest": "12264cb16fca9317e3ba8d5924a2c8f15f790e36d2f29e7b12aaaf77e1beb73d" + }, + { + "name": "post_office", + "unicode": "1F3E3", + "digest": "5e2d896cd646a2eecd5596af9e44ca1fa2745de5cedaf0f6d193b8243201c6cc" + }, + { + "name": "postal_horn", + "unicode": "1F4EF", + "digest": "339aa61fa1567a1d159bb8204d15db889fbb6cc1106f6e1991b4a184d1bc1fc7" + }, + { + "name": "postbox", + "unicode": "1F4EE", + "digest": "ef1a6543fccb9f1009cc3782c51883e51167721a0b49e8ba21e8e6049b216906" + }, + { + "name": "potable_water", + "unicode": "1F6B0", + "digest": "4a2379835660dfa8b6780d662a10d1effab710f471eb9b5e6ade4772ba7e5aeb" + }, + { + "name": "pouch", + "unicode": "1F45D", + "digest": "cbd47ec1a65f5c642773d8ea2e7e57f7041a2d7ed9df05fbdd7bc8743c6dece6" + }, + { + "name": "poultry_leg", + "unicode": "1F357", + "digest": "d416e9464bd58073bd3e32eb06c0da96905609f47b9d667acdc0810e94237584" + }, + { + "name": "pound", + "unicode": "1F4B7", + "digest": "1ac491bb8a91613b2b1faaac4e7b4bc794d2abef69ac79de17d54c824c3ef826" + }, + { + "name": "pouting_cat", + "unicode": "1F63E", + "digest": "ba28d75401d5bb98773acd35aaf173356bae4d5a5520a226559478138364ebdf" + }, + { + "name": "pray", + "unicode": "1F64F", + "digest": "fb0df9c1566014bd2df2a1afd59366b896f20c03ca3516e02e4be44ea556c8ea" + }, + { + "name": "pray_tone1", + "unicode": "1F64F-1F3FB", + "digest": "c6d8cb46e65ad13a92e85f97e018176fd89513f23e899e15d1ad09e3b4009f4b" + }, + { + "name": "pray_tone2", + "unicode": "1F64F-1F3FC", + "digest": "2cd68cbe1ba3254f173ec8136af79cae64873bd0f20480158c3e6babd5a1a442" + }, + { + "name": "pray_tone3", + "unicode": "1F64F-1F3FD", + "digest": "d2e81863f74a87b96335fb108e7b206f28ed18185362ab4d42a3b0523801398b" + }, + { + "name": "pray_tone4", + "unicode": "1F64F-1F3FE", + "digest": "ad1b91254b101d872325c325ebd1f2a6257cfe22e83de88e29dd16ffac191979" + }, + { + "name": "pray_tone5", + "unicode": "1F64F-1F3FF", + "digest": "23f40a11321decbdc6a1d274b9ad571041d261d364d13d1063c306e73ad52254" + }, + { + "name": "prayer_beads", + "unicode": "1F4FF", + "digest": "cb6f8700154f75749cf2642a25c03e255dc18428baf8b57f6bd807c92b83e28d" + }, + { + "name": "princess", + "unicode": "1F478", + "digest": "47b93eb52d757c3c000d9760391ecb942776d883b28050d833fa11612483d8ee" + }, + { + "name": "princess_tone1", + "unicode": "1F478-1F3FB", + "digest": "1e4073c2abdf51a61a1a85a3e063541fe96e9b9ec36ec6f7fb9c98deeb230869" + }, + { + "name": "princess_tone2", + "unicode": "1F478-1F3FC", + "digest": "6a0a5dc447cd887798f908c15972e7a12d28d81f168b92bcb105786ac253bea0" + }, + { + "name": "princess_tone3", + "unicode": "1F478-1F3FD", + "digest": "2f08d22fdfc7a7d66fcd87ae716b811f43077f5bb17fef87f5b7e2aa93700d70" + }, + { + "name": "princess_tone4", + "unicode": "1F478-1F3FE", + "digest": "02129211bf7bf7ff6de35913b7069aee151532d878b8c4f7e24c012e5b09d4b4" + }, + { + "name": "princess_tone5", + "unicode": "1F478-1F3FF", + "digest": "d676f103600b69dbfdb469469a77b9d561ec460ff862befa58ab30ddc909c9f7" + }, + { + "name": "printer", + "unicode": "1F5A8", + "digest": "c44402c87071f8d31d3997abab53ab9f8f7c11434e747380928814ceb6b0a417" + }, + { + "name": "prohibited", + "unicode": "1F6C7", + "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" + }, + { + "name": "projector", + "unicode": "1F4FD", + "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" + }, + { + "name": "punch", + "unicode": "1F44A", + "digest": "5759db1d7093744c74b840bbb4761fb025d6633f8fa539bcb35dcf54fc05ceb6" + }, + { + "name": "punch_tone1", + "unicode": "1F44A-1F3FB", + "digest": "793b3fa2a43c23b2c1e1b48b86ae35e8c4024cd065fac0a0a5ada87cb78d6de3" + }, + { + "name": "punch_tone2", + "unicode": "1F44A-1F3FC", + "digest": "6fc2467e99982ab00b0c352c6f7793d34faf17b16a0312082c9bd1f0709e3938" + }, + { + "name": "punch_tone3", + "unicode": "1F44A-1F3FD", + "digest": "bf747b29952550c5b4d3807b9ed85b5e5d4bcc3265b0e214791f7db547f861fb" + }, + { + "name": "punch_tone4", + "unicode": "1F44A-1F3FE", + "digest": "3b6c0ccb682552f32d6744c438e3af04a1732c67a74bcafb14c723cf526fed87" + }, + { + "name": "punch_tone5", + "unicode": "1F44A-1F3FF", + "digest": "945bae1aa3587cd1dc57d1ec4da18c67a59e0e7150dcc8735e5357b4ea1234ac" + }, + { + "name": "purple_heart", + "unicode": "1F49C", + "digest": "e0eb886e74f22d40d059ff3a089d472af53c6c53de380f428cca140dfd046345" + }, + { + "name": "purse", + "unicode": "1F45B", + "digest": "67d82ff9a4d76148b9d98538d4b786f880058a556e650ec3f93e1632aa42aaa7" + }, + { + "name": "pushpin", + "unicode": "1F4CC", + "digest": "c4de129d5d8744caffeb2f499fcc0bc6b551843938f8166ffecd0de00bda66e3" + }, + { + "name": "pushpin_black", + "unicode": "1F588", + "digest": "80ebac74edb9e8e1f8a219b32a676d318ed73b359cd8193b91b493d775307f63" + }, + { + "name": "put_litter_in_its_place", + "unicode": "1F6AE", + "digest": "b26d3b68bd62d30ecfe75cfaf309a7a0f91e92db0aa18b0b97b97baf0609d4e6" + }, + { + "name": "question", + "unicode": "2753", + "digest": "258e3169bae177fb0f01ed5f9b933f7f02dd2673e12a316af44a0c3729a78a2c" + }, + { + "name": "rabbit", + "unicode": "1F430", + "digest": "9817a7454aeda77d28f63eb13c0dc0a6d9e6c9abe3dcf538b4b3477e494cddb6" + }, + { + "name": "rabbit2", + "unicode": "1F407", + "digest": "67ba57a31b0768a2118faabdcb088f96f1441e1132397f65b6937d523ff7dabb" + }, + { + "name": "race_car", + "unicode": "1F3CE", + "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" + }, + { + "name": "racehorse", + "unicode": "1F40E", + "digest": "36aa3c7123ee7e15600657166032b21b8edeb192cf6d3ada39b5c65001f7fc40" + }, + { + "name": "radio", + "unicode": "1F4FB", + "digest": "b1403f9a883405b909208f52c9474c2d3923681ea0b02609a6e9dc12460319a5" + }, + { + "name": "radio_button", + "unicode": "1F518", + "digest": "9bcdac17b3620331a32f9bb876812231a701eb5a7f696e7d875f877ab92159fc" + }, + { + "name": "radioactive", + "unicode": "2622", + "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" + }, + { + "name": "rage", + "unicode": "1F621", + "digest": "02ac70551fc51478884c133b29539cae58b463c760db38c0aeec1bdf5b282312" + }, + { + "name": "railway_car", + "unicode": "1F683", + "digest": "8490e2ecf94c7c1d1e22fea0d80cc18a49648741009e51984f583b17bbd022e2" + }, + { + "name": "railway_track", + "unicode": "1F6E4", + "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" + }, + { + "name": "rainbow", + "unicode": "1F308", + "digest": "bbd8ecc8d0737948969a3539d2d202e599404e509f1a21bdbb0a0c41c2540522" + }, + { + "name": "raised_hand", + "unicode": "270B", + "digest": "4192881a0d613b4fcb19b1c2d8b83aadee6f0b12170721c8dd7b1ccef6540199" + }, + { + "name": "raised_hand_tone1", + "unicode": "270B-1F3FB", + "digest": "df2e046c99dceb9184c50a777b403d72bfb25ff473d6a4e20bb9a731db64ed8d" + }, + { + "name": "raised_hand_tone2", + "unicode": "270B-1F3FC", + "digest": "ed179299a1c397cd51cf6067d6795d71a3831d35e1ec9eacbf0286c8992c1e7a" + }, + { + "name": "raised_hand_tone3", + "unicode": "270B-1F3FD", + "digest": "cacbd0ddef65bc01a41bd921ea159f8cd89050309b10f15780d6199f79434a54" + }, + { + "name": "raised_hand_tone4", + "unicode": "270B-1F3FE", + "digest": "04c934c7a55b83bcfa7f3880fc1f6aa0f188090c37b9670e6775a512a1cf59e9" + }, + { + "name": "raised_hand_tone5", + "unicode": "270B-1F3FF", + "digest": "da0c4283b7b19861237c023234c6db28045b8f5a5971acb015733e08e2940e86" + }, + { + "name": "raised_hands", + "unicode": "1F64C", + "digest": "308e475f38558e73bd66e28693d77478caa5bca4360cffaffc2a97b5858c56ba" + }, + { + "name": "raised_hands_tone1", + "unicode": "1F64C-1F3FB", + "digest": "e39b9bc49dccc127e44f543e98961fcf5bcd44d6e216741bcd10ec3667263c84" + }, + { + "name": "raised_hands_tone2", + "unicode": "1F64C-1F3FC", + "digest": "f376ab13071ffdc11888ec221ef5b4de546ca0f60bd9ae30bf3da4066c220462" + }, + { + "name": "raised_hands_tone3", + "unicode": "1F64C-1F3FD", + "digest": "67694325a43e629c00fa9bd2ff7e19f84f216b2855ae2cf097762dfa7aca25e6" + }, + { + "name": "raised_hands_tone4", + "unicode": "1F64C-1F3FE", + "digest": "a2254fe75a0770708916a4ddd5db4420221c6ea9db9f74068d14eadfc0f3772c" + }, + { + "name": "raised_hands_tone5", + "unicode": "1F64C-1F3FF", + "digest": "bd7c9897cefb454ccdc46027bf56d6587565bdd345d7d0f081b7b671a53f6c99" + }, + { + "name": "raising_hand", + "unicode": "1F64B", + "digest": "d57178fc77e9fa140682634da35f9ab12a65d9b4c506b7cd8a9697f1b5910bdb" + }, + { + "name": "raising_hand_tone1", + "unicode": "1F64B-1F3FB", + "digest": "f46b34361ef79743f3187d6860182bbe1ae411031db7fe5c0f7292fa472b9c16" + }, + { + "name": "raising_hand_tone2", + "unicode": "1F64B-1F3FC", + "digest": "20b85a2ebca150b2020a04b41d34884c78c22f42c251e2b9d23fd3724574143b" + }, + { + "name": "raising_hand_tone3", + "unicode": "1F64B-1F3FD", + "digest": "5e0401b528c2b8edff766d39cdcedbe9abebe4c940df7a36ace61f59c08d508a" + }, + { + "name": "raising_hand_tone4", + "unicode": "1F64B-1F3FE", + "digest": "e4f5624264269ad09cde207cd7d4eb0fd46de816880daeec457ac8cd51cc1b7b" + }, + { + "name": "raising_hand_tone5", + "unicode": "1F64B-1F3FF", + "digest": "eb34b6c037bee5bbc4222f6aab421aa785f527ebf1b5e971769e5102244d60e1" + }, + { + "name": "ram", + "unicode": "1F40F", + "digest": "b71950d7a286a4c4909c5ec7c35211c2a5c20b6bad341bd863c6a85c4bcf9c80" + }, + { + "name": "ramen", + "unicode": "1F35C", + "digest": "7dd185b24852b577913edc78647cd53b27d42e225fde29aa2f3aba25c980b5c4" + }, + { + "name": "rat", + "unicode": "1F400", + "digest": "7a10d9ba5ee1010d421d9cf73d7966507302a69617a32fe9f1a00d57a31f7bd7" + }, + { + "name": "record_button", + "unicode": "23FA", + "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" + }, + { + "name": "recycle", + "unicode": "267B", + "digest": "74a54ed62a40dfbdcace1f08b085658a77d45c62570273927ad270bf9a8a2f4d" + }, + { + "name": "red_car", + "unicode": "1F697", + "digest": "558730d6418aa5d85b73af58c8041efd12cff906e26ea47c50963f66d33d6eb8" + }, + { + "name": "red_circle", + "unicode": "1F534", + "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307" + }, + { + "name": "registered", + "unicode": "00AE", + "digest": "ed924107384461aabb4924c401c6c087ffa047bc2ef735823e7c2be67804707c" + }, + { + "name": "relaxed", + "unicode": "263A", + "digest": "65072f7b9bfaaa92b8a0ed012dffe2cfd2efa3748264aaf450aa31ba6bd44045" + }, + { + "name": "relieved", + "unicode": "1F60C", + "digest": "1f2c7ae6a9d74a112de89403be6eca3d8155d70395e7fce51032fc961f235c7d" + }, + { + "name": "reminder_ribbon", + "unicode": "1F397", + "digest": "e4a2afc7dce40589657f7043ba8acc9638fd4117252278233ea89f84cddad387" + }, + { + "name": "repeat", + "unicode": "1F501", + "digest": "27b6dad9215e58e24c607a39dbf398ecf66ccb692c81e08eb2f5f4912db30522" + }, + { + "name": "repeat_one", + "unicode": "1F502", + "digest": "052d13f2b08eaf70b31252aa78f95d06fbe22c58945c19381b13cbeb1c855651" + }, + { + "name": "restroom", + "unicode": "1F6BB", + "digest": "b77fbc4247c241362e5ef9e6eb58b1b437aa9d16b65886cec0c55ceb55c1440e" + }, + { + "name": "revolving_hearts", + "unicode": "1F49E", + "digest": "2b8925d3e78df2dba8534252fe60bf03285346f6b3697be7668bd568e6d85931" + }, + { + "name": "rewind", + "unicode": "23EA", + "digest": "91a95b26d12ca76111556096f4d96484c9f1d7e1b20ccff5a3291b36e529a6d1" + }, + { + "name": "ribbon", + "unicode": "1F380", + "digest": "9c0296d8c2baa84c99347c431bf79b288d98b5f17b1ce7605ad7ce1da265d5aa" + }, + { + "name": "rice", + "unicode": "1F35A", + "digest": "e34849496a79e71ae4700df94f2a54895bf6de758a92edeae33fe78295a3ba21" + }, + { + "name": "rice_ball", + "unicode": "1F359", + "digest": "52df5da8b0edbdeb56d66e0f30ad4549abdd81c064f7269d920dcac66a3df2e4" + }, + { + "name": "rice_cracker", + "unicode": "1F358", + "digest": "d55f8f9d807f4619eb243c510938067a7417a64bd9435b05dfeb2a36fdb2b6a0" + }, + { + "name": "rice_scene", + "unicode": "1F391", + "digest": "482d854d8d30edfc1ecd48a4ce476e6498606321405bf5a0b4ff74489a092af8" + }, + { + "name": "right_speaker", + "unicode": "1F568", + "digest": "d268bb84be863c0884620dfc6d2a764b0c7466d2f9810549b138e21ac70add4e" + }, + { + "name": "right_speaker_one", + "unicode": "1F569", + "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" + }, + { + "name": "right_speaker_three", + "unicode": "1F56A", + "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" + }, + { + "name": "ring", + "unicode": "1F48D", + "digest": "ae2a93e7895b9b89f5a39f01d356ffed988f219ef8b658a56c55285826a4533b" + }, + { + "name": "ringing_bell", + "unicode": "1F56D", + "digest": "d71ab7fa937fc4af507b5b07ea58a4f31e875d9e8304ef2b850d7cebe0e9cd66" + }, + { + "name": "robot", + "unicode": "1F916", + "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" + }, + { + "name": "rocket", + "unicode": "1F680", + "digest": "65d8bd005ceac41904237b7a8c5f55f16713a55d971522f0bbe63a1d548e515d" + }, + { + "name": "roller_coaster", + "unicode": "1F3A2", + "digest": "907baab1f3d7becf3f8a3b1264642b395bd73b4af49e23058b3abb5c69e9106a" + }, + { + "name": "rolling_eyes", + "unicode": "1F644", + "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" + }, + { + "name": "rooster", + "unicode": "1F413", + "digest": "6cefdaa45631ed8c9480e15f578c793d95af81b42687164fd7900eee325ccf07" + }, + { + "name": "rose", + "unicode": "1F339", + "digest": "584909a4a2ece625c688f8479a39692bb8e816b692e6eb7dfd40cb045259b1b2" + }, + { + "name": "rosette", + "unicode": "1F3F5", + "digest": "0ce3b85ca05124ab99d57ebc9aa17bb246ee614d2fcda1ef62bf42ac7e616148" + }, + { + "name": "rosette_black", + "unicode": "1F3F6", + "digest": "ae8675891c88f9d98463d35178445950c39b0deb0f0e8b3f341228a6e0d0e477" + }, + { + "name": "rotating_light", + "unicode": "1F6A8", + "digest": "369e069e0bfecc7413e75f4015e9c1de527a33c7cce3f6c2b4adb60a0d9d338c" + }, + { + "name": "round_pushpin", + "unicode": "1F4CD", + "digest": "1bc5fe5a90a6e56ea00246f1b008a0e0cce0d77c226dc0300bf9a2804b543877" + }, + { + "name": "rowboat", + "unicode": "1F6A3", + "digest": "c10e09bf8be8b1a8ef3113edd9327126d6a4644f3bc81c7ada2922851e4d1cfb" + }, + { + "name": "rowboat_tone1", + "unicode": "1F6A3-1F3FB", + "digest": "a84fc1b30d1a284dcd3899dc4de8f11e7b65c258528eb41c7dbf8f82425fee12" + }, + { + "name": "rowboat_tone2", + "unicode": "1F6A3-1F3FC", + "digest": "85f001430a2ad607a15901f7c2dcf8381471f42d6cc0775e76a2ff1f457151c1" + }, + { + "name": "rowboat_tone3", + "unicode": "1F6A3-1F3FD", + "digest": "adf8b1e45a46a13f3db40c29df0312216558e9d0c615aa46a8e913cee5003a81" + }, + { + "name": "rowboat_tone4", + "unicode": "1F6A3-1F3FE", + "digest": "05482749ec40bdf02e53fc42d316c51f4f3ed643f21e8fc16b81930e4a884bda" + }, + { + "name": "rowboat_tone5", + "unicode": "1F6A3-1F3FF", + "digest": "d4bb337d948996d4a23d87f99988f02fc207815b862082ffd2eef5f0c1016aa9" + }, + { + "name": "rugby_football", + "unicode": "1F3C9", + "digest": "e14aebbded78d4a5e9b4028f79a8ca840d02798c6758cb9e926e992e2a35a4f3" + }, + { + "name": "runner", + "unicode": "1F3C3", + "digest": "58a884f06d37b0ce78197bebcd3f0e102dd90022ebd86ec70a2ef5a5cdf9683b" + }, + { + "name": "runner_tone1", + "unicode": "1F3C3-1F3FB", + "digest": "65f1633d1517803de23686d2dbcc75a5787874266db4981138ccdbe4badc773c" + }, + { + "name": "runner_tone2", + "unicode": "1F3C3-1F3FC", + "digest": "2bc81f3fb77445cdc75c34806ab0ce912bacfe47f63b5d2011a4f5d370cf7064" + }, + { + "name": "runner_tone3", + "unicode": "1F3C3-1F3FD", + "digest": "beaf5f254cba2991fdd0c38ce2ddd1b4c1110e15b2b7bc026d32f162e295c4ef" + }, + { + "name": "runner_tone4", + "unicode": "1F3C3-1F3FE", + "digest": "21d531ba9b3d13747ad636b8f7a6f184c974bf61d9f529975a64f9629263c407" + }, + { + "name": "runner_tone5", + "unicode": "1F3C3-1F3FF", + "digest": "b02a5bcc58cc45f8219262ec44c77764172fd8f2624d9122ded4a5a5db04c0ed" + }, + { + "name": "running_shirt_with_sash", + "unicode": "1F3BD", + "digest": "431bed35f4a55175bf99af769e74a81e8650c6ab34af6ecddaa1417ff7e437e6" + }, + { + "name": "sa", + "unicode": "1F202", + "digest": "a47a480631f874e8a2cd69b5d513f90a1e81a96bfa2f6025bf244a82baca3656" + }, + { + "name": "sagittarius", + "unicode": "2650", + "digest": "14871e6681c35e4a63a0b19613f77b3674d00cb78d06975e02ca29e61b5cea8c" + }, + { + "name": "sailboat", + "unicode": "26F5", + "digest": "6f742dde6c180a174b771aa3942b558e98a3dc1eb212dd31add86c5fa5620865" + }, + { + "name": "sake", + "unicode": "1F376", + "digest": "aa1392790c805950779dde7778292c937f8c1aaecb522876171d5ee542ec51f8" + }, + { + "name": "sandal", + "unicode": "1F461", + "digest": "14f1e9003a6acd90a55f23c48ed87a758fca586f2e0b0edc4dc9d1deef9eb067" + }, + { + "name": "santa", + "unicode": "1F385", + "digest": "12feddd84eb49ce30ae68d4f93d66e2c0dd11297a4d1275c9a50d4f35bea83a9" + }, + { + "name": "santa_tone1", + "unicode": "1F385-1F3FB", + "digest": "a75813770efe27d5b4c80ad892d0c796d88d1a0dbb1bd02d5f68882d7abad479" + }, + { + "name": "santa_tone2", + "unicode": "1F385-1F3FC", + "digest": "90f8072fdde5f4a275cbd1902d6c94689d453b1bee0336213dc9d6f7e1d038e1" + }, + { + "name": "santa_tone3", + "unicode": "1F385-1F3FD", + "digest": "0973053e7b77d268080126a50b95b45429630e5d49f62210e7b71840794c7dc5" + }, + { + "name": "santa_tone4", + "unicode": "1F385-1F3FE", + "digest": "5cd49c0d199a42846b400b3c1244d448ed6fe5ce993d379817cb2a5f7c0b609b" + }, + { + "name": "santa_tone5", + "unicode": "1F385-1F3FF", + "digest": "a54c36dfa99b39549fb1d3dd7f0021a7aee28112960172ed466dacc67961c525" + }, + { + "name": "satellite", + "unicode": "1F4E1", + "digest": "3b9797c8161526edce0bd8e9b8563055166f9307761c367ab3e2ad7645b6dee0" + }, + { + "name": "satellite_orbital", + "unicode": "1F6F0", + "digest": "104b135e3736a4bcfd51a42dadb53bf3e00d7f85d77a94bcb86c6704fbfacd01" + }, + { + "name": "saxophone", + "unicode": "1F3B7", + "digest": "1090da174ce8aa4f7d35025f65d5ac235e09310abde998d2a725ef3a989a2b75" + }, + { + "name": "scales", + "unicode": "2696", + "digest": "b2984caa182b691a33650344708f47c61d6d319fd067760d7594c2ef60c1e27b" + }, + { + "name": "school", + "unicode": "1F3EB", + "digest": "caf35260dc465a833521e4a0034201978fed41bbf72cd770756b3340c60e8a0c" + }, + { + "name": "school_satchel", + "unicode": "1F392", + "digest": "a89a2cc46d24d57c2d6b95ed7a56ed829ae2f97b9e6201b2d5adc78c2b78518b" + }, + { + "name": "scissors", + "unicode": "2702", + "digest": "a4e91127ac83acf5ebc64fbeca768cbbf24f2f0a484861c9c8104bee377b97ae" + }, + { + "name": "scorpion", + "unicode": "1F982", + "digest": "a090a96731bc1171b054b51abec4c9b36faa62708fd51ac48277ccf5e55d9d12" + }, + { + "name": "scorpius", + "unicode": "264F", + "digest": "1ad9bc1030a8f58f3f3223bac52c954cc7a0350805a9df7a42a26972c3b74728" + }, + { + "name": "scream", + "unicode": "1F631", + "digest": "75d613786737ee9c0a74da7394b9ae190eacc7182164627ad8205ac64e4cc09a" + }, + { + "name": "scream_cat", + "unicode": "1F640", + "digest": "eee04ff27c2c6b57d698cb87b0af8064ba8313ffc13aa090e38cd5aa8c3d2f76" + }, + { + "name": "scroll", + "unicode": "1F4DC", + "digest": "b8205847649e3ce6b946f1d1da972ed015adde3841c62971b8169235f4b41c1f" + }, + { + "name": "seat", + "unicode": "1F4BA", + "digest": "054c4db0bc8939e9dd951a3f73e9ae4b3c31652784f4d304b509c2bd32f98e31" + }, + { + "name": "secret", + "unicode": "3299", + "digest": "77daef6e5c91d55228781ddec954a7089d1851297ec81daef6e813cd22915b5e" + }, + { + "name": "see_no_evil", + "unicode": "1F648", + "digest": "aa5883fe605aeaa172d16640b8347580f9cb7d85a596da1b13955f27b0b79297" + }, + { + "name": "seedling", + "unicode": "1F331", + "digest": "a75ec929402de1e653fd6bc89e5be2f92fe5fe52f39e4b6c290eae3c59172b56" + }, + { + "name": "seven", + "unicode": "0037-20E3", + "digest": "c6a34020f6bb25871164fad44302a45c5bffced87f51dfbb816c2985ad7f6a1c" + }, + { + "name": "shamrock", + "unicode": "2618", + "digest": "530e6b987ecb9bcbf0d6e0e11bd075e7949873c784da4f9e1e1b47efd37e5058" + }, + { + "name": "shaved_ice", + "unicode": "1F367", + "digest": "fc22c3568f6be56771e83fd0e67b7eb3750041304d5d4979d3ec417f5201230e" + }, + { + "name": "sheep", + "unicode": "1F411", + "digest": "3e3656b82784164ca02c5d775db7245260f0119d2c1d35ba552a6dc75ef02544" + }, + { + "name": "shell", + "unicode": "1F41A", + "digest": "ff2f4f574b61bffd85c63bc2315c80d3cbcaba37a7c15a1f00783d312bd441d4" + }, + { + "name": "shield", + "unicode": "1F6E1", + "digest": "062aec4a325da7b637c5710846c7e7319229be49b7e59f50428442a7ef725d60" + }, + { + "name": "shinto_shrine", + "unicode": "26E9", + "digest": "9768fe94142a7dc169703d3707b203f285a546455e29fe2bbf185d44f160d6d0" + }, + { + "name": "ship", + "unicode": "1F6A2", + "digest": "f8d5b0c8ec66287b732d9171ac1913be02efb656de11501213a207d8a6c801e1" + }, + { + "name": "shirt", + "unicode": "1F455", + "digest": "e2e72c323f3bfaea02e8cf52201aa144dc56ec0f25ec97d5f04ee6c2ee99104e" + }, + { + "name": "shopping_bags", + "unicode": "1F6CD", + "digest": "0194ba540c47e4fc6403be2df68f785d56810efc2dc011dfbf700f3778cb704a" + }, + { + "name": "shower", + "unicode": "1F6BF", + "digest": "c945120182392510348de9a957c2b77a4645d118691298a2ad660dafa62a859c" + }, + { + "name": "signal_strength", + "unicode": "1F4F6", + "digest": "7876ed9d602e1be746ca0629f072d85668d1f9715e9135745e803bdf89819a3c" + }, + { + "name": "six", + "unicode": "0036-20E3", + "digest": "b409f23b73e46393c7a814442816b5880c38ef12a7feb5505e71276c195e8ca9" + }, + { + "name": "six_pointed_star", + "unicode": "1F52F", + "digest": "4bc294dcbf4185250873b52b2fb5453fb7d80df912db929add6e4b7efc066363" + }, + { + "name": "ski", + "unicode": "1F3BF", + "digest": "7ee81a2e2f7ff4e32dbf3d64b034e7542ec0c86d32e25eb125052e674943d75f" + }, + { + "name": "skier", + "unicode": "26F7", + "digest": "49df9a4206ae0c7c2dbfc8a8b13fd3e14e6f7e750bd5a8581ab6a1626d4c165e" + }, + { + "name": "skull", + "unicode": "1F480", + "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" + }, + { + "name": "skull_crossbones", + "unicode": "2620", + "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" + }, + { + "name": "sleeping", + "unicode": "1F634", + "digest": "4ead95079b1a542eedd0e5a0e93fddb318a002bdaffaa2fe5d8d7f20bf8143ed" + }, + { + "name": "sleeping_accommodation", + "unicode": "1F6CC", + "digest": "10ee8cd925a75d7977b7cf004e08b5a8147b509ee4281e879a8b57c4a7c2cb04" + }, + { + "name": "sleepy", + "unicode": "1F62A", + "digest": "dea3b246bb8af1b28e200358e3d5d59c8bba1813f35a7f4a57ec568ef43591db" + }, + { + "name": "slight_frown", + "unicode": "1F641", + "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" + }, + { + "name": "slight_smile", + "unicode": "1F642", + "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" + }, + { + "name": "slot_machine", + "unicode": "1F3B0", + "digest": "9d516b389299431b608c89d3f02ac68d28cb8df2a780f2048923bbcfbb49f416" + }, + { + "name": "small_blue_diamond", + "unicode": "1F539", + "digest": "97389e82755dc43015089dee635072357ec347f0117b2d3e9b006c46514948ee" + }, + { + "name": "small_orange_diamond", + "unicode": "1F538", + "digest": "67442d3b707501b7768f606115688373d13617ecf0b3b03ace0f1a6d38f66ddf" + }, + { + "name": "small_red_triangle", + "unicode": "1F53A", + "digest": "e0a556a3dd5bbf0290ed7c00eb6f6307dc2ea98d1fb3111fd85a7f46242a3638" + }, + { + "name": "small_red_triangle_down", + "unicode": "1F53B", + "digest": "7a11dcb8a517df220493d471759e4f4bca0db3769e2d942bbf596a88a3e57f72" + }, + { + "name": "smile", + "unicode": "1F604", + "digest": "46a7c3545b0038dfce6825d97544f6665f28512ad05c404d668e32ac599c7ecb" + }, + { + "name": "smile_cat", + "unicode": "1F638", + "digest": "c1db961f0fa261532b842816aca7ea7f6d8b461c7e930a1a1c91f96efd9db515" + }, + { + "name": "smiley", + "unicode": "1F603", + "digest": "deeaaee64ebdd9fc0bcb719db75c3f7e0c33ddbcc97f6cd51f9f84377a4368ce" + }, + { + "name": "smiley_cat", + "unicode": "1F63A", + "digest": "85ad852cb3881c4b754af172fdfc6231af42578033ea9f2981ceae944c41e72f" + }, + { + "name": "smiling_imp", + "unicode": "1F608", + "digest": "e777bdf186d89921df106d23bf002967b69afffd7e981b3cbb19f89630a06e87" + }, + { + "name": "smirk", + "unicode": "1F60F", + "digest": "2e7fddd8bed33ef4b7d8c13320302b87a28203e576ef87bd43716952cf0b5ace" + }, + { + "name": "smirk_cat", + "unicode": "1F63C", + "digest": "9ca0721f4c18592b4b809ade8f716b95fa30cd31dd87d1e41db29a319becd705" + }, + { + "name": "smoking", + "unicode": "1F6AC", + "digest": "3d14b3f0c57eb7a6a31ff371b0a454986533b79dbbeac78a76e4063478911b8d" + }, + { + "name": "snail", + "unicode": "1F40C", + "digest": "57d946c7ec84dfad71bc4f7a042927ec5712aef50c66d21af892b6c8a7faf5e1" + }, + { + "name": "snake", + "unicode": "1F40D", + "digest": "d084da540162288721364992f3b8059cbf2efd9f5b48f49a196ddbe23a073870" + }, + { + "name": "snowboarder", + "unicode": "1F3C2", + "digest": "de9e1767526de606f4908743af94cc17e89fdb0a2a44167d3d021ef09d033ab9" + }, + { + "name": "snowflake", + "unicode": "2744", + "digest": "e476863ccd7d7b549c6191fb25c121c6a467b4baef4683b7dc3e0a793c2e5d76" + }, + { + "name": "snowman", + "unicode": "26C4", + "digest": "792946b8446f2243d11b89d07c73a774be3abd36573f3918640b1ba8714270b5" + }, + { + "name": "snowman2", + "unicode": "2603", + "digest": "571acabaa4d55782c4529b762423a7e34cb1fb6bb7852cbd013e2e846d8311d1" + }, + { + "name": "sob", + "unicode": "1F62D", + "digest": "562f02ab584bcbcf9ba73cf7fa7d7129965266abd28db2c73913b8c42f2f5aca" + }, + { + "name": "soccer", + "unicode": "26BD", + "digest": "5fd0d534659b63dc862c65a80561b255bece0b76708fe8ecbae8e01b08d8cad0" + }, + { + "name": "soon", + "unicode": "1F51C", + "digest": "d2a1ab16a4056d80c827ea23f9332bb73235fc841b857cbf545062ff8aeed81d" + }, + { + "name": "sos", + "unicode": "1F198", + "digest": "fadfe8337e133a6f05d205d0807f288e5c230db04cb09f3547ce0cb73cfcf48a" + }, + { + "name": "sound", + "unicode": "1F509", + "digest": "c0074b338fd461f1f9d1143b7f9b3781ddb3fd501ea79b2410630433a8e87b83" + }, + { + "name": "space_invader", + "unicode": "1F47E", + "digest": "d264390004bd28d664dfda0069104be6db32ce477e23a95ac595bac2e29fd4e7" + }, + { + "name": "spades", + "unicode": "2660", + "digest": "d1ad99a4fc20dfea881a9062a9f2109e483dbb5dea3b29e9653cb27ec57b4800" + }, + { + "name": "spaghetti", + "unicode": "1F35D", + "digest": "ac63f9ad143e236ce6068098e5330a333ade9cddfb3dd6b1457ea47ce9dcf7e9" + }, + { + "name": "sparkle", + "unicode": "2747", + "digest": "95b8f4f1bb6080cd1d7bd333c4724dbba43ed196dce72a2bbaab46c4a1bc0e48" + }, + { + "name": "sparkler", + "unicode": "1F387", + "digest": "3a296e4d0081ad1a566e111d218e352e1439bba9fd04e8a1eb9a8e36bd438cb7" + }, + { + "name": "sparkles", + "unicode": "2728", + "digest": "5ab280ea10c30e0e0b5a26ef52b8f47ad44a983330f7ef62ac0c0888752bbdb6" + }, + { + "name": "sparkling_heart", + "unicode": "1F496", + "digest": "f145dab6b597c07e5a851176fabaf56dd857209645483d1acc1490d12c969113" + }, + { + "name": "speak_no_evil", + "unicode": "1F64A", + "digest": "6eae2d066d39c4ba81e58a8327ed875c68bc9b1297c18dc0f5243e477a81040f" + }, + { + "name": "speaker", + "unicode": "1F508", + "digest": "ea59c5a9d994808ff7937c300303e644b5f1ad41097e82f9e73ea6e1c718936c" + }, + { + "name": "speaking_head", + "unicode": "1F5E3", + "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" + }, + { + "name": "speech_balloon", + "unicode": "1F4AC", + "digest": "5dccfda46fc984583bc9eaece66e7e884f2a9eb12a69dbd3493035e3c862edd0" + }, + { + "name": "speech_left", + "unicode": "1F5E8", + "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" + }, + { + "name": "speech_right", + "unicode": "1F5E9", + "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" + }, + { + "name": "speech_three", + "unicode": "1F5EB", + "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" + }, + { + "name": "speech_two", + "unicode": "1F5EA", + "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" + }, + { + "name": "speedboat", + "unicode": "1F6A4", + "digest": "553a288ab8eeb3dee7b9d1c92eba38016caef7658beaa828136ba1d6ba8ed08a" + }, + { + "name": "spider", + "unicode": "1F577", + "digest": "519f7243b5574102ce3f8953e5480812830a1feb32ae51e8573724c864338481" + }, + { + "name": "spider_web", + "unicode": "1F578", + "digest": "42959fae08a2162d6ee8c8706f823c5932f3801bc90da30d2ca9a48c3ff25572" + }, + { + "name": "spy", + "unicode": "1F575", + "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" + }, + { + "name": "spy_tone1", + "unicode": "1F575-1F3FB", + "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" + }, + { + "name": "spy_tone2", + "unicode": "1F575-1F3FC", + "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" + }, + { + "name": "spy_tone3", + "unicode": "1F575-1F3FD", + "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" + }, + { + "name": "spy_tone4", + "unicode": "1F575-1F3FE", + "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" + }, + { + "name": "spy_tone5", + "unicode": "1F575-1F3FF", + "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" + }, + { + "name": "stadium", + "unicode": "1F3DF", + "digest": "4356db5d2cdef8c40830638debaf1f50831130c12ae8d8dc3d9a6bd28fdaa1f7" + }, + { + "name": "star", + "unicode": "2B50", + "digest": "13240b8fada84e7555892996e9f9652503bf9b9a002056c2bae428d543abe2da" + }, + { + "name": "star2", + "unicode": "1F31F", + "digest": "9b56c7548f6a222499d4e848576ea25eab837db72b207ebf8a62a451b35f758f" + }, + { + "name": "star_and_crescent", + "unicode": "262A", + "digest": "10b8a0771e415aa6610fa62185137aa1836c2bb3e82f1a3f601470e94f784923" + }, + { + "name": "star_of_david", + "unicode": "2721", + "digest": "5bc4d1038b8316281e01a9c575ded7ede0fc24c7593db5b5d36ca2e188aa5614" + }, + { + "name": "stars", + "unicode": "1F320", + "digest": "23605eafc949feead3eca145a7ff5ee3b211a8bfd95621bd35dd05df532b97c6" + }, + { + "name": "station", + "unicode": "1F689", + "digest": "c346f12fff64161041af8492550c3541a6304e53f30288224ddd0c6fe08c4d6b" + }, + { + "name": "statue_of_liberty", + "unicode": "1F5FD", + "digest": "56fa27ab059a9fd1f53aec47d9108277a3bf04a73186f36297cd1207c832ee31" + }, + { + "name": "steam_locomotive", + "unicode": "1F682", + "digest": "d0ec2eb3d761ab6157e17eab1b8b4dec3a69f9becc4251592cbb67d71825e661" + }, + { + "name": "stereo", + "unicode": "1F4FE", + "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" + }, + { + "name": "stew", + "unicode": "1F372", + "digest": "12e6e4bf48a7296700e07a053d831dd67b70c308ca9522ca96e933a4d1ef6c5e" + }, + { + "name": "stock_chart", + "unicode": "1F5E0", + "digest": "4a0fbf54d19b0b5626f91c932a24e6ac12a65b4fc276d852ff4356c8c579d28a" + }, + { + "name": "stop_button", + "unicode": "23F9", + "digest": "57310962c7738a7da4f2a62cbd5e0b26d7aec357978267a0d8ca8e6cbd7ffb02" + }, + { + "name": "stopwatch", + "unicode": "23F1", + "digest": "c8e69c24f9da98dcb41c9c6355922d08a702f12a35667fbc5beb3f659430333d" + }, + { + "name": "straight_ruler", + "unicode": "1F4CF", + "digest": "55ff7182a3696461df52e3000708083f803bc8bf0f3c25dacb34175cc104b51d" + }, + { + "name": "strawberry", + "unicode": "1F353", + "digest": "fd501e1fefb70242ac7c4dc30ad3d8c3ae200b263a832daedaa984906114afaf" + }, + { + "name": "stuck_out_tongue", + "unicode": "1F61B", + "digest": "1b49956cec511ee382177d95da77c8b6a9214a02c86bf7c6c6fd6cc9df3e9331" + }, + { + "name": "stuck_out_tongue_closed_eyes", + "unicode": "1F61D", + "digest": "60a4d5d92550c6ad4db901d42c9f6434fe94fa3ddb353b6019a93d374d9485e9" + }, + { + "name": "stuck_out_tongue_winking_eye", + "unicode": "1F61C", + "digest": "d9c15ad1c4782a0391a79aeda2745127527385b0b5fc01c8d96c3f3b637a74ae" + }, + { + "name": "sun_with_face", + "unicode": "1F31E", + "digest": "56b14e92f68f8701fdc42763e1f4695ed352845f22bd5d412f827e5cf98dd83b" + }, + { + "name": "sunflower", + "unicode": "1F33B", + "digest": "817dea222a75bb6492c32b4b144d07f48295d7dd113e21760f90b18277612ebb" + }, + { + "name": "sunglasses", + "unicode": "1F60E", + "digest": "16003cc5256397389889f52e0a5e14daea8d8c72f2ea660b8174529868cba9cd" + }, + { + "name": "sunny", + "unicode": "2600", + "digest": "f68a774b7d574fc711111e17368b57c40d973d263c7e857544a09051d4592ab9" + }, + { + "name": "sunrise", + "unicode": "1F305", + "digest": "ce06a9321bc04605538a59f9fca8536d6209d7ded03120e5d2a0be955bb17ddf" + }, + { + "name": "sunrise_over_mountains", + "unicode": "1F304", + "digest": "286244ac2bec8c5c41cf8c7c439702fa525c57fab623f7f9bd7687db0adf75b2" + }, + { + "name": "surfer", + "unicode": "1F3C4", + "digest": "d17c7ea185ca5ef5a2950ef126ee14103bf7769acb419a20d08cc023f619e459" + }, + { + "name": "surfer_tone1", + "unicode": "1F3C4-1F3FB", + "digest": "af66f2f26071b3ba8d7c795139055a58a857212f8cb1f51a507242ad7d2c49c7" + }, + { + "name": "surfer_tone2", + "unicode": "1F3C4-1F3FC", + "digest": "7a34e8b1fdad0a89bbb10333d241583ef018517fdd90f171ad7121de53776a3f" + }, + { + "name": "surfer_tone3", + "unicode": "1F3C4-1F3FD", + "digest": "b2f4cbd59a0aa93c7ee2bbb14ce55c8306dc25884377982a5f132ce6c074fa1d" + }, + { + "name": "surfer_tone4", + "unicode": "1F3C4-1F3FE", + "digest": "b16a02cfcc3606524cca9408e69c654fb83a162eaec8faae8dfd8ec67fe391c5" + }, + { + "name": "surfer_tone5", + "unicode": "1F3C4-1F3FF", + "digest": "b9a156e1aa57544b703db4e4a7773e244a3139e82c2c808c2e5a804fb524f512" + }, + { + "name": "sushi", + "unicode": "1F363", + "digest": "d2709b51ee92997c7fafa1b1517259cb896819c8dc9ba98ae26e1d44ec810d4f" + }, + { + "name": "suspension_railway", + "unicode": "1F69F", + "digest": "48903e103ef00a068b0100b28319b1e41c6a4485cb564f0ca59422ec9d3b259c" + }, + { + "name": "sweat", + "unicode": "1F613", + "digest": "8d684fa882bcbf07f4e91ea02a48cd61f22e7aa206162b8352c26fc19361ed4e" + }, + { + "name": "sweat_drops", + "unicode": "1F4A6", + "digest": "fca48e255dff08dab97ef98b75c67f7504a13be8b90afac88b69a7b7e887e445" + }, + { + "name": "sweat_smile", + "unicode": "1F605", + "digest": "0c8156554eec2396b5fee908da46484945db980d2ebc6dee57b4069a86826182" + }, + { + "name": "sweet_potato", + "unicode": "1F360", + "digest": "3ce74ea9bc14906a3d29a9592c0657aee8f7961d406992752f7580b16ca6bdd0" + }, + { + "name": "swimmer", + "unicode": "1F3CA", + "digest": "05f3aa8544e3b15837bb06ae47344633b3e60d64c572dc6638c4cee19d6e5506" + }, + { + "name": "swimmer_tone1", + "unicode": "1F3CA-1F3FB", + "digest": "85a266a9131f6a1b37e758305ca43ffb46e3e07b0a465c5faefbdb5e5adeb7a4" + }, + { + "name": "swimmer_tone2", + "unicode": "1F3CA-1F3FC", + "digest": "f2afdc4d05a2694e663a420d5ad82bd48c92aedc4137d0fd3725bf08c41bd12a" + }, + { + "name": "swimmer_tone3", + "unicode": "1F3CA-1F3FD", + "digest": "b87ecc38fb9e8eeeef8b120164d758d3f6a68a407053b03261354fd7f90f43b6" + }, + { + "name": "swimmer_tone4", + "unicode": "1F3CA-1F3FE", + "digest": "a08629cf3484953b851b357c6a04891fb97ac15e70c376bbb82af47479835e1c" + }, + { + "name": "swimmer_tone5", + "unicode": "1F3CA-1F3FF", + "digest": "21d83f66b2ef3e348f9e14ec108b9a90262d9934039ebd573471d2bdcde68974" + }, + { + "name": "symbols", + "unicode": "1F523", + "digest": "f33c3ce58374e23b8957c759016fdb5c56ef7fe812bd4e693ae8ff7574cf6bbf" + }, + { + "name": "synagogue", + "unicode": "1F54D", + "digest": "b13402c3c5793ebf924335a87a9f69befb7a6c152fc2a288261b2c2d49842eb6" + }, + { + "name": "syringe", + "unicode": "1F489", + "digest": "39e5e7530255ccf2ff35ec5c653568c8645a4711170c573117f796ea3438c44a" + }, + { + "name": "taco", + "unicode": "1F32E", + "digest": "6b004ce7129e00abcc10278bba1b9c3d5ac71888b99bf353f9878d8e494e3e0d" + }, + { + "name": "tada", + "unicode": "1F389", + "digest": "956a180a1f18e3a1252761e5b3713324f63975ee1fe32168b59b60aa4dd8b72b" + }, + { + "name": "tanabata_tree", + "unicode": "1F38B", + "digest": "d074457ba347687bfc8397ec62edee6325c411356216e7d43acd3f60628a0bb8" + }, + { + "name": "tangerine", + "unicode": "1F34A", + "digest": "1b46bb690458914220cba18c43d7ae0f6914adfee6dba7cf2bb58ed4e1854ad8" + }, + { + "name": "taurus", + "unicode": "2649", + "digest": "ea87fb3baa32605107d63b60847e4873ad9e21b7e7b652e3721cde777168670d" + }, + { + "name": "taxi", + "unicode": "1F695", + "digest": "f44249c643a96d924e1eb35f67a133f3ca61128e610a880afaa09a73c7bcaf9d" + }, + { + "name": "tea", + "unicode": "1F375", + "digest": "56ab8c291de8320c5b339e1cfbe972696e4ea31c592cefa240eda9a3abdf4fa3" + }, + { + "name": "telephone", + "unicode": "260E", + "digest": "609104588e00039199a2fef3190ee6a7be5fca7cb09b36ffe5a7d800aac69d8d" + }, + { + "name": "telephone_black", + "unicode": "1F57F", + "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" + }, + { + "name": "telephone_receiver", + "unicode": "1F4DE", + "digest": "e3bf6034de6cf2160893ba4990eba198185a6a3f9cd5767a63b048e41c297640" + }, + { + "name": "telephone_white", + "unicode": "1F57E", + "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" + }, + { + "name": "telescope", + "unicode": "1F52D", + "digest": "abe0aca5f2c78105b0e9e4c8ee7a40adcd9bb013e7c49d568076459bade73556" + }, + { + "name": "ten", + "unicode": "1F51F", + "digest": "7593aa7ffe7192a2e35c6ccec76522f6243777783c9152c7c03419835ea58c03" + }, + { + "name": "tennis", + "unicode": "1F3BE", + "digest": "0a5fad3f7f35da0f37761e2279c148dbe154fa14c0e2a0749209b8b2b213a388" + }, + { + "name": "tent", + "unicode": "26FA", + "digest": "7ddf437d8d186e4e3c3e818d137518d590fa06098813c7fe20e1f2a9704feab2" + }, + { + "name": "thermometer", + "unicode": "1F321", + "digest": "597d1714442698a22187fee4d57a2580322f7206c7d51e4519023824598ec08f" + }, + { + "name": "thermometer_face", + "unicode": "1F912", + "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" + }, + { + "name": "thinking", + "unicode": "1F914", + "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" + }, + { + "name": "thought_balloon", + "unicode": "1F4AD", + "digest": "76c8513191641f0a79e878ccc0d83c4576984609810633f596db2f64cc684b7d" + }, + { + "name": "thought_left", + "unicode": "1F5EC", + "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" + }, + { + "name": "thought_right", + "unicode": "1F5ED", + "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" + }, + { + "name": "three", + "unicode": "0033-20E3", + "digest": "ca0147a8f67cea3bc2516fa8deef4325188359559786c94ff0b27f90eef04b88" + }, + { + "name": "thumbs_down_reverse", + "unicode": "1F593", + "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" + }, + { + "name": "thumbs_up_reverse", + "unicode": "1F592", + "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" + }, + { + "name": "thumbsdown", + "unicode": "1F44E", + "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" + }, + { + "name": "thumbsdown_tone1", + "unicode": "1F44E-1F3FB", + "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" + }, + { + "name": "thumbsdown_tone2", + "unicode": "1F44E-1F3FC", + "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" + }, + { + "name": "thumbsdown_tone3", + "unicode": "1F44E-1F3FD", + "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" + }, + { + "name": "thumbsdown_tone4", + "unicode": "1F44E-1F3FE", + "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" + }, + { + "name": "thumbsdown_tone5", + "unicode": "1F44E-1F3FF", + "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" + }, + { + "name": "thumbsup", + "unicode": "1F44D", + "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" + }, + { + "name": "thumbsup_tone1", + "unicode": "1F44D-1F3FB", + "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" + }, + { + "name": "thumbsup_tone2", + "unicode": "1F44D-1F3FC", + "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" + }, + { + "name": "thumbsup_tone3", + "unicode": "1F44D-1F3FD", + "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" + }, + { + "name": "thumbsup_tone4", + "unicode": "1F44D-1F3FE", + "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" + }, + { + "name": "thumbsup_tone5", + "unicode": "1F44D-1F3FF", + "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" + }, + { + "name": "thunder_cloud_rain", + "unicode": "26C8", + "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" + }, + { + "name": "ticket", + "unicode": "1F3AB", + "digest": "a7654a5529535120da3c377e72cd1f7997bdc2dabf1d44b584f7df7852b158f9" + }, + { + "name": "tickets", + "unicode": "1F39F", + "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" + }, + { + "name": "tiger", + "unicode": "1F42F", + "digest": "9ebe3117f5f1b589ff8164f8d87dcc275923e0db87121d2cee0fdb9b56dfc4ac" + }, + { + "name": "tiger2", + "unicode": "1F405", + "digest": "212c95dc60d52420a6320917fe3fdd0683b4edc1a2a2c4a1c60920d1f90f4bc3" + }, + { + "name": "timer", + "unicode": "23F2", + "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" + }, + { + "name": "tired_face", + "unicode": "1F62B", + "digest": "ad687a956388ec53ca1e301a0abe2f1e2cfb9f73cd543dd61a21c7335a42e332" + }, + { + "name": "tm", + "unicode": "2122", + "digest": "1156c8b0af40b336bbb6534b3302ac63eab009c4cd0476adcf1fc4669f04b647" + }, + { + "name": "toilet", + "unicode": "1F6BD", + "digest": "a4a24529c21e00e0861f4160c771f0e90aae8f6aee7550ad30d3dbb3fabbd4be" + }, + { + "name": "tokyo_tower", + "unicode": "1F5FC", + "digest": "6324f154f5f5c722044129e5bca03484aca1439911585e42c1c181ffa30b480c" + }, + { + "name": "tomato", + "unicode": "1F345", + "digest": "41bb6de095b27815eacb74a70aea8f7d4fe1ff947182b112001dd47ae7e45fbb" + }, + { + "name": "tone1", + "unicode": "1F3FB", + "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c" + }, + { + "name": "tone2", + "unicode": "1F3FC", + "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f" + }, + { + "name": "tone3", + "unicode": "1F3FD", + "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8" + }, + { + "name": "tone4", + "unicode": "1F3FE", + "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3" + }, + { + "name": "tone5", + "unicode": "1F3FF", + "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81" + }, + { + "name": "tongue", + "unicode": "1F445", + "digest": "bf9dd7c65a8dc5d77eb013658a0a12a13f7b224a784e65e203d9584bb6b41427" + }, + { + "name": "tools", + "unicode": "1F6E0", + "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" + }, + { + "name": "top", + "unicode": "1F51D", + "digest": "d645030099aeb433307569e8e1c4342c1c411a8fefe50fdca7a3207a1a0db671" + }, + { + "name": "tophat", + "unicode": "1F3A9", + "digest": "1082fb2ee2e98fe65d21081b74ca59b07adef85043e2d36f25cac69db2d31fd3" + }, + { + "name": "track_next", + "unicode": "23ED", + "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" + }, + { + "name": "track_previous", + "unicode": "23EE", + "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" + }, + { + "name": "trackball", + "unicode": "1F5B2", + "digest": "8332503454ce42059d720c285fe2b15eb0562a0a4b234dccb0f3159bb30a91aa" + }, + { + "name": "tractor", + "unicode": "1F69C", + "digest": "a41d304c41a85d966f6a7c301735fdbe2ae41f4471dd7dcd72023046ca2546d0" + }, + { + "name": "traffic_light", + "unicode": "1F6A5", + "digest": "005f68d028fec8d9ae389cc2b23e1343a82c028eb32820d5e56f5c84eba315d1" + }, + { + "name": "train", + "unicode": "1F68B", + "digest": "bf32893b7b9ecd248e8afe840624061746ac6ceb741e3e861ebfa46014f4bed4" + }, + { + "name": "train2", + "unicode": "1F686", + "digest": "08a9732453a0b4f68dd2d3d3879f04ee538f65897913b5a5157c0585132a374a" + }, + { + "name": "train_diesel", + "unicode": "1F6F2", + "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" + }, + { + "name": "tram", + "unicode": "1F68A", + "digest": "5a86d31f7ab677d967fecd75babc900b5169766d0228961912314c4c4d1d64ee" + }, + { + "name": "triangle_round", + "unicode": "1F6C6", + "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" + }, + { + "name": "triangular_flag_on_post", + "unicode": "1F6A9", + "digest": "d824c973d84cd62c845d64e546de87b094fda8f9972b6a33acd75e1a5ac19f75" + }, + { + "name": "triangular_ruler", + "unicode": "1F4D0", + "digest": "5576802d8bcb8836f473d9c7641ff666250c23c8476c676b253e577695025959" + }, + { + "name": "trident", + "unicode": "1F531", + "digest": "70c1e8254da5b0e4552673b487503a20feeb249484d4596836b75de70220be82" + }, + { + "name": "triumph", + "unicode": "1F624", + "digest": "b09262121b0d3d9d017ded22d0fbb1acaa6ee8c9d38e9ac34292b390d97408fe" + }, + { + "name": "trolleybus", + "unicode": "1F68E", + "digest": "5af943836cc30c3b79160c70b6488c984fa63c104dce08c436597a93d30ff6f4" + }, + { + "name": "trophy", + "unicode": "1F3C6", + "digest": "c249938815042716db2b39cdece6715fabf9e56ed583270c451925e6c91f9191" + }, + { + "name": "tropical_drink", + "unicode": "1F379", + "digest": "352d903e813a27d2a74803322539b50a50aec0ca2ed7ab4a92ec480b1c226cb6" + }, + { + "name": "tropical_fish", + "unicode": "1F420", + "digest": "13a104ca9c326238ab8d85b60759629b4efaa836946fbe58d78d779443475f7b" + }, + { + "name": "truck", + "unicode": "1F69A", + "digest": "13d381d6b43b42350a1e24c02296904b8fdc38c1bf0939fc7037850127e91f21" + }, + { + "name": "trumpet", + "unicode": "1F3BA", + "digest": "df7fb48920ac0919ee2d7b30102016479f747a5d4dd25b3e18d9f17121d232d1" + }, + { + "name": "tulip", + "unicode": "1F337", + "digest": "519a84336464b5dc8db57eecef3e5b8ed82ccfdaa0ed0fa9ef7bcf0e8acea1f8" + }, + { + "name": "turkey", + "unicode": "1F983", + "digest": "e87bff52ad3e301dc62f6832b8a6fcaf99db260a96263e4203a55ce3abda8cf8" + }, + { + "name": "turned_ok_hand", + "unicode": "1F58F", + "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" + }, + { + "name": "turtle", + "unicode": "1F422", + "digest": "388b3e75b931638a09f65b842d26e2cc87b200ba782dec871f84cddd71aaeaf3" + }, + { + "name": "tv", + "unicode": "1F4FA", + "digest": "dba03be6482d6291599c7393b0f749c0de5c873d45c96a20ccc53b3e104a6a24" + }, + { + "name": "twisted_rightwards_arrows", + "unicode": "1F500", + "digest": "5fcad0247576e10e683f353008749975e9371a4f66c0901a73c3a0c7803c63c7" + }, + { + "name": "two", + "unicode": "0032-20E3", + "digest": "20ad722532a5073fff8aef0a5e890421da0ae97f0723a8a2cc503c13d24ba597" + }, + { + "name": "two_hearts", + "unicode": "1F495", + "digest": "160cb11e3ed2ae1b20957d445c6c4b4bd604d067294818dfeeefba4562425eb9" + }, + { + "name": "two_men_holding_hands", + "unicode": "1F46C", + "digest": "923734704e544f7484fdb424bfe26f51ee07754db712cd151f8fbe955023a1ab" + }, + { + "name": "two_women_holding_hands", + "unicode": "1F46D", + "digest": "58a40e7819cab3589ac81bb4fdc485b7196ee355544b54c6b00169028c260130" + }, + { + "name": "u5272", + "unicode": "1F239", + "digest": "b7e8ad52629a1f1fca77a5c9a51da87ce2b9a81f6af9bcbe9bec9552d398e9bf" + }, + { + "name": "u5408", + "unicode": "1F234", + "digest": "f359799d206cff6aae3af26eb8ad153abd38e817d4c70b2e5e5e8cf2f46e645e" + }, + { + "name": "u55b6", + "unicode": "1F23A", + "digest": "c40293bea0f148e76ca5152e830b1b474380fe259180fbf74fece1ccc9afd8a3" + }, + { + "name": "u6307", + "unicode": "1F22F", + "digest": "45449f7ae29da9e507c19d0f2b22f17f7cbd763f2ec87eb893be5bae49c7f78e" + }, + { + "name": "u6708", + "unicode": "1F237", + "digest": "b897ead8c952013975ce6f381cdb8c584ebe4015311ef87f2a332c8a9e155d75" + }, + { + "name": "u6709", + "unicode": "1F236", + "digest": "8b2f792abc1313a1a58f2fb8b37ad68a964004c962535f7739131257b1331a05" + }, + { + "name": "u6e80", + "unicode": "1F235", + "digest": "fd982a56d4c492e63526b427bb948d7f155b0d5c414a68c7177698a71e72269b" + }, + { + "name": "u7121", + "unicode": "1F21A", + "digest": "334f87a5254b58503d9f7a8ecc3d971a99839ec9c22c443469d72caca1750a48" + }, + { + "name": "u7533", + "unicode": "1F238", + "digest": "3c8e743ae9960e43b9fa0cc698018fcb2a52ae34d143f0561298191f9def019c" + }, + { + "name": "u7981", + "unicode": "1F232", + "digest": "a08bf39be3a54c076de79478c09b79c5c4d221853722870dd6e81abb78a4b64a" + }, + { + "name": "u7a7a", + "unicode": "1F233", + "digest": "5dfb74a534a6490df989f84eac271c79d52f29313b6d43662dd0ff029794367c" + }, + { + "name": "umbrella", + "unicode": "2614", + "digest": "ff1191f6c11b82f5337f78aadb58af50c69abaf676a384b0473bf49004e4018f" + }, + { + "name": "umbrella2", + "unicode": "2602", + "digest": "aa7db9d6ed42dff847a8e5ee48a8eeff7a6e7f30de155a28951407f5aaa3dae2" + }, + { + "name": "unamused", + "unicode": "1F612", + "digest": "efbbcaee6f3178afe509d74d13243ec6befe3112620a01e5079171eac4b32417" + }, + { + "name": "underage", + "unicode": "1F51E", + "digest": "ae9a300fa400a57b7216a0a040fb8a5f02236fbceeeceed58bfd953c87ad51fe" + }, + { + "name": "unicorn", + "unicode": "1F984", + "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" + }, + { + "name": "unlock", + "unicode": "1F513", + "digest": "63dbef0855399254ae01cf4ef0676adebc1432ae1ee260b569c23ae8152deaf8" + }, + { + "name": "up", + "unicode": "1F199", + "digest": "902a3ecbcd73099a28476b49bc9e7b06da6cc002ee584e0501e5b625fb515088" + }, + { + "name": "upside_down", + "unicode": "1F643", + "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" + }, + { + "name": "urn", + "unicode": "26B1", + "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" + }, + { + "name": "v", + "unicode": "270C", + "digest": "df85ad1a3ff365c3232a010701c9b25cd824d19fa2511422dee60ac231f457e3" + }, + { + "name": "v_tone1", + "unicode": "270C-1F3FB", + "digest": "ce45db8de862b6f37d9208920d7c7c19335fac2cbff59b52be1ccbc01e3249da" + }, + { + "name": "v_tone2", + "unicode": "270C-1F3FC", + "digest": "9036c8d793b02b4d2e6a4752b8ec319ec50efd6fcd6feef7b0671a63e5659acc" + }, + { + "name": "v_tone3", + "unicode": "270C-1F3FD", + "digest": "a94b95f7656d62b442c99f2643b96b0c6114683401a94cdda68405c37efecc4c" + }, + { + "name": "v_tone4", + "unicode": "270C-1F3FE", + "digest": "5c75f74993856f2faeeaee68df7689056e60d30e8c573039db8303167f7d0a80" + }, + { + "name": "v_tone5", + "unicode": "270C-1F3FF", + "digest": "bb899672adb3c11f65983fbf9581de7f0a1bbac86fde146e799cea1126fe241e" + }, + { + "name": "vertical_traffic_light", + "unicode": "1F6A6", + "digest": "36296e03620f16d35e5cec195cd97f5b358dfdedcd43bc1b3f7988ff7e85ab47" + }, + { + "name": "vhs", + "unicode": "1F4FC", + "digest": "f4be55f4c23a85e0caacbf569742c117c8fd52c189465a6560cbd2f8873ad74f" + }, + { + "name": "vibration_mode", + "unicode": "1F4F3", + "digest": "b9b8dfa3160c22f78b7d627cb52636d81ca6230a196cee5e94028e32e06b9a98" + }, + { + "name": "video_camera", + "unicode": "1F4F9", + "digest": "3bfaa24e5fb00145e3e4dd07ecf569dabbb3f211551e46085ef23cf23002cfc3" + }, + { + "name": "video_game", + "unicode": "1F3AE", + "digest": "4dcbd76030e37d0f7429852991a5f3f126cbdedfc124ecad0ba29d227375f6e2" + }, + { + "name": "violin", + "unicode": "1F3BB", + "digest": "8ab7adc6e1e934f9e05009cd0a6d4da3136092c8f11c0606b91914be182206f5" + }, + { + "name": "virgo", + "unicode": "264D", + "digest": "aaa19752756d0cac949445de1d2b8bf1f75a071368ae0acf5002f4acdc34826f" + }, + { + "name": "volcano", + "unicode": "1F30B", + "digest": "86c17d61d66bfa868c02f1d31daca22f077c096368ef53cd9bfb9914a2f0b273" + }, + { + "name": "volleyball", + "unicode": "1F3D0", + "digest": "b505684b13f814fbc08dc8ff652849328f46068276e0a24ae1961e2aff15868f" + }, + { + "name": "vs", + "unicode": "1F19A", + "digest": "e31bd8b48b88c21d717964d1360a7751684dd1e0b63fdd655f1a9ec10a952dfb" + }, + { + "name": "vulcan", + "unicode": "1F596", + "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" + }, + { + "name": "vulcan_tone1", + "unicode": "1F596-1F3FB", + "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" + }, + { + "name": "vulcan_tone2", + "unicode": "1F596-1F3FC", + "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" + }, + { + "name": "vulcan_tone3", + "unicode": "1F596-1F3FD", + "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" + }, + { + "name": "vulcan_tone4", + "unicode": "1F596-1F3FE", + "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" + }, + { + "name": "vulcan_tone5", + "unicode": "1F596-1F3FF", + "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" + }, + { + "name": "walking", + "unicode": "1F6B6", + "digest": "8ec0b2207d4368422261bc58944c17dff2554b2356becfb18f21dd87425cd67b" + }, + { + "name": "walking_tone1", + "unicode": "1F6B6-1F3FB", + "digest": "9ee2224226326833fb0c9598c737fbd2f6bca1c81f082537e9f22ea1de4ff48e" + }, + { + "name": "walking_tone2", + "unicode": "1F6B6-1F3FC", + "digest": "4855d521e937d10d58eeb2bbada493699e31e1098128f81a9e3303bcf3edeb49" + }, + { + "name": "walking_tone3", + "unicode": "1F6B6-1F3FD", + "digest": "82669cf7167054a3615add01059f87dbb809edac3889ee171d5994de90448000" + }, + { + "name": "walking_tone4", + "unicode": "1F6B6-1F3FE", + "digest": "c11f03aa96248272f831f68b93c5b21b2ecbffeb1b4c1c13373bf539ee7db8f8" + }, + { + "name": "walking_tone5", + "unicode": "1F6B6-1F3FF", + "digest": "18238ee121a64211f6bcdbd475cee4ad6debe2bf421daba53d125aa005c26d10" + }, + { + "name": "waning_crescent_moon", + "unicode": "1F318", + "digest": "96ef03ff85247877255a5ca3e8a8bb63f7d41f66531e8db61cbcd863e3ad7355" + }, + { + "name": "waning_gibbous_moon", + "unicode": "1F316", + "digest": "994223113ad151e6b42ee317a10dad18f86759a308e61ab88eeb10ab780aae67" + }, + { + "name": "warning", + "unicode": "26A0", + "digest": "a702e51efd1a3ab425eada008ccf694f38a71db14bb710edacc2e206d61f5ca3" + }, + { + "name": "wastebasket", + "unicode": "1F5D1", + "digest": "afecb31aaf5078298ab9f7c5da29a49ce0cdefe477ee50889be9c0e43ccf1799" + }, + { + "name": "watch", + "unicode": "231A", + "digest": "410334c87b8552f601f4ea1b7e36582a8b22f11b804d5ab1008d4af2b5a0cbe6" + }, + { + "name": "water_buffalo", + "unicode": "1F403", + "digest": "d1becfaea464372c46e5442c6030ea355806ce5864c2435c123a9bb3a2c3c5eb" + }, + { + "name": "watermelon", + "unicode": "1F349", + "digest": "88dd78812520c44080c79fe8cb1825bc713e5155da2ce8c73286333749e7035e" + }, + { + "name": "wave", + "unicode": "1F44B", + "digest": "5103c49914ff1a2d76a1ab6db2530ddd9f48b98b708ab15292ceadf28873c939" + }, + { + "name": "wave_tone1", + "unicode": "1F44B-1F3FB", + "digest": "ef2d79f377d09dedd1e900b2f4e4a2412bf562cd88484f71c52d465053f8aae9" + }, + { + "name": "wave_tone2", + "unicode": "1F44B-1F3FC", + "digest": "d323e6e2e9ce035bc11b98226d46ab393dfdf3909d99e7a828b51950e6574656" + }, + { + "name": "wave_tone3", + "unicode": "1F44B-1F3FD", + "digest": "8a8a386d53252455c20d6b235c462fd9cb3b20c9c19c67e67b3dece4621b5cf6" + }, + { + "name": "wave_tone4", + "unicode": "1F44B-1F3FE", + "digest": "a8281c2ab9cf6e2b3d3cad24707fe412ec2398195530b716a2617477416c0432" + }, + { + "name": "wave_tone5", + "unicode": "1F44B-1F3FF", + "digest": "5ccbee95bfc180580c8a02b88146110c4d132b8ea618dd6a58f03c1db921d58d" + }, + { + "name": "wavy_dash", + "unicode": "3030", + "digest": "b5b67fc12938801a98ff22b6f7b566c603f58c183737fa740a500724879f0e99" + }, + { + "name": "waxing_crescent_moon", + "unicode": "1F312", + "digest": "20446122d170b18f88ea71524f6747d42b97f9d765c52e676e5163fee58ec379" + }, + { + "name": "waxing_gibbous_moon", + "unicode": "1F314", + "digest": "4324e43d4d45e6333f7379c9feb8efd3093d76f3920d7dc5ad3c615e76104998" + }, + { + "name": "wc", + "unicode": "1F6BE", + "digest": "cb7c5d35bf11149d12cda2c0897cb6038e043127055bbe2e8e33c9b422d6d8fc" + }, + { + "name": "weary", + "unicode": "1F629", + "digest": "29a291033a1b67eda3710dffae42d63fcfa663e37dab728c236172f3e877fe8f" + }, + { + "name": "wedding", + "unicode": "1F492", + "digest": "6c7d874f464c9c76b0d767135aa40ced94089b5f71d373098b47488d7f3ef7c4" + }, + { + "name": "whale", + "unicode": "1F433", + "digest": "94168acda6ba502b64ea50ff4aaafb7e6258d7c6806e91f090c8a3c46edc5b6d" + }, + { + "name": "whale2", + "unicode": "1F40B", + "digest": "e1cde2308bd510b2449c96e88ffec796856f98b19ceedc1cd7e9ea009dae1417" + }, + { + "name": "wheel_of_dharma", + "unicode": "2638", + "digest": "bbd6927697c22a1c3e56fd0c9933d9e00dbf120505fe48d02cb486bcd67a8b2c" + }, + { + "name": "wheelchair", + "unicode": "267F", + "digest": "513f759acf528f6a7e39d9de1d171c3faebe645c9cf3bd86b185123016beef95" + }, + { + "name": "white_check_mark", + "unicode": "2705", + "digest": "a0b3bf7c4fb131e7a9fab5169ea4094e2665e02cedaa091f0d6e78609b2f17ed" + }, + { + "name": "white_circle", + "unicode": "26AA", + "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c" + }, + { + "name": "white_flower", + "unicode": "1F4AE", + "digest": "a3efea4950e09994f5e9d3d16f0728969238302304a6cce90b293c56e9a3e20c" + }, + { + "name": "white_large_square", + "unicode": "2B1C", + "digest": "99c4442a65f2e3c568f45aed9e74590206c517a716557f4d741d967c9f42ed40" + }, + { + "name": "white_medium_small_square", + "unicode": "25FD", + "digest": "a1edfeb4e540dcc020ba5dde19f7a18d90966788baa5382a22a0f9038d593f01" + }, + { + "name": "white_medium_square", + "unicode": "25FB", + "digest": "794c2339ca71bb6d65ac488fb7b5dc4f0a2412f30890d2c4ece53cdbf52ba78b" + }, + { + "name": "white_small_square", + "unicode": "25AB", + "digest": "9c4c308070a0c4524993cc36feaa778aad8f0df9f209b82d28b1f3811c441bc4" + }, + { + "name": "white_square_button", + "unicode": "1F533", + "digest": "f46e18c7250c874d1b4d6117eda741d86a081352e76f3d019dd64af2669fa4bb" + }, + { + "name": "white_sun_cloud", + "unicode": "1F325", + "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" + }, + { + "name": "white_sun_rain_cloud", + "unicode": "1F326", + "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" + }, + { + "name": "white_sun_small_cloud", + "unicode": "1F324", + "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" + }, + { + "name": "wind_blowing_face", + "unicode": "1F32C", + "digest": "20bdeb8e39dc637792ac9fbee031c5791889f3126e83556ba51f98809c19763c" + }, + { + "name": "wind_chime", + "unicode": "1F390", + "digest": "1fc26f33ce13b6a969bb76e914de054ec5d1c7c4cd1dc5ee8fea5f3149f794d8" + }, + { + "name": "wine_glass", + "unicode": "1F377", + "digest": "7dfcf9c5195a20fd2745b19e102910392b0fc8f1650b98ab81957807841935e0" + }, + { + "name": "wink", + "unicode": "1F609", + "digest": "404ac6c920414ca35894da1d97b3b2fabe92bd09569274eb5798fbb297129036" + }, + { + "name": "wolf", + "unicode": "1F43A", + "digest": "ebadd7766c4a314b4027c32435a2f5727a6283123dfb8834e10251cbfc07ca2f" + }, + { + "name": "woman", + "unicode": "1F469", + "digest": "9f0dbb5d1e0db4f008141582dcb6413f5aebaa13e191349c976a435b2bee0956" + }, + { + "name": "woman_tone1", + "unicode": "1F469-1F3FB", + "digest": "c1f2a503481fdd96cfbfa7d556500f8e0da0cea1c72ed1078ecbb6962221c22a" + }, + { + "name": "woman_tone2", + "unicode": "1F469-1F3FC", + "digest": "bf78b3a8f7424037069f8ac337e154ef185f55026c71a6cf6dbe15eb42ef9813" + }, + { + "name": "woman_tone3", + "unicode": "1F469-1F3FD", + "digest": "4ccd70a2052b932b3395ac0a957c05815327dc8082fd461abcd797411db8ce05" + }, + { + "name": "woman_tone4", + "unicode": "1F469-1F3FE", + "digest": "71b5efc4a410102e60048ca05f87587384a6db309f3be94109a4f92ea97072dc" + }, + { + "name": "woman_tone5", + "unicode": "1F469-1F3FF", + "digest": "91a1cd015731f4db501c276a8236eb0665e4dc7aa1891e2a67b8d3e543fbea9c" + }, + { + "name": "womans_clothes", + "unicode": "1F45A", + "digest": "599332c0b863a40fd0c319e4e0f52ae847326a96d180c288e0466b3ac308a27e" + }, + { + "name": "womans_hat", + "unicode": "1F452", + "digest": "231ff55c3fa56d8fb5731fe41f547e67ffacfdde82286f45d4ca65a2d2821239" + }, + { + "name": "womens", + "unicode": "1F6BA", + "digest": "f971429456b543804412490af2e27e0b14d0d536a156db898bce67b136e1b563" + }, + { + "name": "worried", + "unicode": "1F61F", + "digest": "e017f636e79b9301f3a06471a5f3513ba7dbb9b97938de1140c1df4c32fd8844" + }, + { + "name": "wrench", + "unicode": "1F527", + "digest": "c9ded4f7f496bad8691677226310bbd31bb485722ea479bc7a68a2b4ef9d55d9" + }, + { + "name": "writing_hand", + "unicode": "1F58E", + "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" + }, + { + "name": "writing_hand_tone1", + "unicode": "270D-1F3FB", + "digest": "38e64e6dca4847a12aef8a117c113b2025d841501c4bc8188c57d0c8a4f1e34d" + }, + { + "name": "writing_hand_tone2", + "unicode": "270D-1F3FC", + "digest": "2b2d0ac2701ae707c31d9c85feb2e3700e11398701e2b0519338897817d53baf" + }, + { + "name": "writing_hand_tone3", + "unicode": "270D-1F3FD", + "digest": "85d67f90ff8bd2e7157f28fd857e6730b660a7eb82eb5350f57671f728ce725b" + }, + { + "name": "writing_hand_tone4", + "unicode": "270D-1F3FE", + "digest": "056c05c201b3d0972433f00910967ad7334e37726e2956fee053ec2e1a9153c7" + }, + { + "name": "writing_hand_tone5", + "unicode": "270D-1F3FF", + "digest": "95c59157d301ee08990e4302fd9bdd7953e1d1abed09636d0837d84e44f53ba6" + }, + { + "name": "x", + "unicode": "274C", + "digest": "1d256b0015b9cbdeaa4558f9241782c89d86c79a42e507621f7949c56a90b6c0" + }, + { + "name": "yellow_heart", + "unicode": "1F49B", + "digest": "e869a80266b4379a8d82988fef25e187632bfb076ae619f576e416906cd688a7" + }, + { + "name": "yen", + "unicode": "1F4B4", + "digest": "8f3d801c687e585e4497123c5c91a8b0c558578deec6a8c1591b25e64a3a8992" + }, + { + "name": "yin_yang", + "unicode": "262F", + "digest": "e8ea4c686518ad6165e15ed67b529f2f1e20d648aa2ecb7e9bff5a6067dd3fea" + }, + { + "name": "yum", + "unicode": "1F60B", + "digest": "d9c97bbf6bdb6e39977437680f0b37c9335306c51e01114056ae1d4c9c85b0e0" + }, + { + "name": "zap", + "unicode": "26A1", + "digest": "37588734c7fe330ae35e6ee99e7cf4183e8fe1bc01f6bbbc6293b21076a338cb" + }, + { + "name": "zero", + "unicode": "0030-20E3", + "digest": "519c927db8264d5379ab2c6a18656ea6dd1ceb2afc92eb48563bf86af4697571" + }, + { + "name": "zipper_mouth", + "unicode": "1F910", + "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" + }, + { + "name": "zzz", + "unicode": "1F4A4", + "digest": "f07c56d2d55c0a886c26a8e3d49a9adeab54cc1a0c0354ea8d3bf23aaed3176d" + } +] \ No newline at end of file diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 197e826e5bc..340fc5452ab 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -8,7 +8,7 @@ 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 @@ -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 @@ -292,7 +292,7 @@ module API end class Label < Grape::Entity - expose :name, :color + expose :name, :color, :description end class Compare < Grape::Entity @@ -334,7 +334,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 e5ae88eb96f..1fee1dee1a6 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -111,17 +111,21 @@ module API # 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? diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 78ca58ad0d1..4af6bef0fa7 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/projects.rb b/lib/api/projects.rb index 6fcb5261e40..24b31005475 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -244,6 +244,34 @@ module API end end + # Archive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/archive + post ':id/archive' do + authorize!(:archive_project, user_project) + + user_project.archive! + + present user_project, with: Entities::Project + end + + # Unarchive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/unarchive + post ':id/unarchive' do + authorize!(:archive_project, user_project) + + user_project.unarchive! + + present user_project, with: Entities::Project + end + # Remove project # # Parameters: diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 783fcfb61ad..4fc3443ac68 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -48,4 +48,23 @@ class AwardEmoji JSON.parse(File.read(json_path)) end end + + # Returns an Array of Emoji names and their asset URLs. + def self.urls + @urls ||= begin + path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + prefix = Gitlab::Application.config.assets.prefix + digest = Gitlab::Application.config.assets.digest + + JSON.parse(File.read(path)).map do |hash| + if digest + fname = "#{hash['unicode']}-#{hash['digest']}" + else + fname = hash['unicode'] + end + + { name: hash['name'], path: "#{prefix}/#{fname}.png" } + end + end + end end diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb index 905c4c0144e..3eb544dfef9 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 34c38913474..f21dbef216c 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}/ + 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 856f56fb175..fac7dad3243 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 470727ee312..b469ea0f626 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -43,7 +43,7 @@ module Banzai end def url_for_object(range, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 713a56ba949..bd88207326c 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -37,7 +37,7 @@ module Banzai end def url_for_object(commit, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index 207437ba7cf..d25de900674 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 edc26386903..37344b90576 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -35,15 +35,29 @@ module Banzai def call # Early return if the project isn't using an external tracker - return doc if project.nil? || project.default_issues_tracker? + return doc if project.nil? || default_issues_tracker? - replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| - issue_link_filter(content) + 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 8d368f3b9e7..d179bea181e 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 f31f921903b..7ce26db1b90 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 diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8147e5ed3c7..a2987850d03 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -31,7 +31,7 @@ module Banzai end def url_for_object(label, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index 0659fed1419..9b209533a89 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 57c71708992..cad38a51851 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(mr, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index e88b27c1fae..4cb82178024 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 7141ed7c9bd..e589b5df6ec 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 132f0a4bd93..31386cf851c 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 86d484feb90..96fdb06304e 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 41380627d39..ea21c7b041c 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 e8011519608..42dbab9d27e 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 c870a42f741..d507eb5ebe1 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(snippet, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 8c5855e5ffc..62a79c62e20 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 4056dcd6d64..a4eda6fdf76 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 f642aee0967..7edfe5ade2d 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 24f16f8b547..eea3af842b6 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -59,13 +59,28 @@ module Banzai end def call - replace_text_nodes_matching(User.reference_pattern) do |content| - user_link_filter(content) + return doc if project.nil? + + ref_pattern = User.reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + user_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + user_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(User.reference_pattern) do |link, text| - user_link_filter(link, link_text: text) - end + doc end # Replace `@user` user references in text with links to the referenced @@ -90,7 +105,7 @@ module Banzai private def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def link_class diff --git a/lib/banzai/filter/yaml_front_matter_filter.rb b/lib/banzai/filter/yaml_front_matter_filter.rb index e4e2f3f228d..58e3e81209e 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 f60966c3c0f..321fd5bbe14 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/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index 9b4ff0f0f80..0b5a9e0b2b8 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -1,5 +1,3 @@ -require 'banzai' - module Banzai module Pipeline class WikiPipeline < FullPipeline diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb new file mode 100644 index 00000000000..28a2391dbf8 --- /dev/null +++ b/lib/gitlab/badge/build.rb @@ -0,0 +1,24 @@ +module Gitlab + module Badge + ## + # Build badge + # + class Build + def initialize(project, ref) + @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref) + end + + def to_s + @image[:name].sub(/\.svg$/, '') + end + + def type + 'image/svg+xml' + end + + def data + File.read(@image[:path]) + end + end + end +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 761b63e98f6..1acc22fe5bf 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -21,7 +21,6 @@ module Gitlab default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 41f0edcaf7e..8f9be6cd9a3 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,7 +5,7 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index d4b6f6d120d..97ef9851d71 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -63,6 +63,10 @@ module Gitlab end def reply_key + key_from_to_header || key_from_additional_headers + end + + def key_from_to_header key = nil message.to.each do |address| key = Gitlab::IncomingEmail.key_from_address(address) @@ -72,6 +76,17 @@ module Gitlab key end + def key_from_additional_headers + reply_key = nil + + Array(message.references).each do |message_id| + reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id) + break if reply_key + end + + reply_key + end + def sent_notification return nil unless reply_key diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb index 431d50882fd..2152182b37f 100644 --- a/lib/gitlab/fogbugz_import/client.rb +++ b/lib/gitlab/fogbugz_import/client.rb @@ -26,7 +26,7 @@ module Gitlab def user_map users = {} res = @api.command(:listPeople) - res['people']['person'].each do |user| + [res['people']['person']].flatten.each do |user| users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] } end users diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index a1c6ee7bd69..78d7a4f27cf 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -34,16 +34,21 @@ module Gitlab @source_project = source_project @current_user = current_user @original_html = markdown(text) + @pattern = Gitlab::ReferenceExtractor.references_pattern end def rewrite(target_project) - pattern = Gitlab::ReferenceExtractor.references_pattern + return @text unless needs_rewrite? - @text.gsub(pattern) do |reference| + @text.gsub(@pattern) do |reference| unfold_reference(reference, Regexp.last_match, target_project) end end + def needs_rewrite? + @text =~ @pattern + end + private def unfold_reference(reference, match, target_project) diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb new file mode 100644 index 00000000000..abc8c8c55e6 --- /dev/null +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -0,0 +1,51 @@ +module Gitlab + module Gfm + ## + # Class that rewrites markdown links for uploads + # + # Using a pattern defined in `FileUploader` it copies files to a new + # project and rewrites all links to uploads in in a given text. + # + # + class UploadsRewriter + def initialize(text, source_project, _current_user) + @text = text + @source_project = source_project + @pattern = FileUploader::MARKDOWN_PATTERN + end + + def rewrite(target_project) + return @text unless needs_rewrite? + + @text.gsub(@pattern) do |markdown| + file = find_file(@source_project, $~[:secret], $~[:file]) + return markdown unless file.try(:exists?) + + new_uploader = FileUploader.new(target_project) + new_uploader.store!(file) + new_uploader.to_markdown + end + end + + def needs_rewrite? + files.any? + end + + def files + referenced_files = @text.scan(@pattern).map do + find_file(@source_project, $~[:secret], $~[:file]) + end + + referenced_files.compact.select(&:exists?) + end + + private + + def find_file(project, secret, file) + uploader = FileUploader.new(project, secret) + uploader.retrieve_from_store!(file) + uploader.file + end + end + end +end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 9068d79c95e..8ce9d32abe0 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,13 +1,10 @@ module Gitlab module IncomingEmail class << self - def enabled? - config.enabled && address_formatted_correctly? - end + FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze - def address_formatted_correctly? - config.address && - config.address.include?("%{key}") + def enabled? + config.enabled && config.address end def reply_address(key) @@ -24,6 +21,13 @@ module Gitlab match[1] end + def key_from_fallback_reply_message_id(message_id) + match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX) + return unless match + + match[1] + end + def config Gitlab.config.incoming_email end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 71cf6a0d886..18523e0aefe 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -41,7 +41,7 @@ module Gitlab data[:issue] = note.noteable.hook_attrs elsif note.for_merge_request? data[:merge_request] = note.noteable.hook_attrs - elsif note.for_project_snippet? + elsif note.for_snippet? data[:snippet] = note.noteable.hook_attrs end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb new file mode 100644 index 00000000000..5132177de51 --- /dev/null +++ b/lib/gitlab/routing.rb @@ -0,0 +1,13 @@ +module Gitlab + module Routing + # Returns the URL helpers Module. + # + # This method caches the output as Rails' "url_helpers" method creates an + # anonymous module every time it's called. + # + # Returns a Module. + def self.url_helpers + @url_helpers ||= Gitlab::Application.routes.url_helpers + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 6f0d02cafd1..f301d42939d 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,7 +1,8 @@ module Gitlab class UrlBuilder - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers include GitlabRoutingHelper + include ActionView::RecordIdentifier def initialize(type) @type = type @@ -37,19 +38,16 @@ module Gitlab namespace_project_commit_url(namespace_id: note.project.namespace, id: note.commit_id, project_id: note.project, - anchor: "note_#{note.id}") + anchor: dom_id(note)) elsif note.for_issue? issue = Issue.find(note.noteable_id) - issue_url(issue, - anchor: "note_#{note.id}") + issue_url(issue, anchor: dom_id(note)) elsif note.for_merge_request? merge_request = MergeRequest.find(note.noteable_id) - merge_request_url(merge_request, - anchor: "note_#{note.id}") - elsif note.for_project_snippet? + merge_request_url(merge_request, anchor: dom_id(note)) + elsif note.for_snippet? snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, - anchor: "note_#{note.id}") + project_snippet_url(snippet, anchor: dom_id(note)) end end end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index cfaf4a129b1..7ec00a898fd 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -1,19 +1,39 @@ -# This task will generate a standard and Retina sprite of all of the current -# Gemojione Emojis, with the accompanying SCSS map. -# -# It will not appear in `rake -T` output, and the dependent gems are not -# included in the Gemfile by default, because this task will only be needed -# occasionally, such as when new Emojis are added to Gemojione. - -begin - require 'sprite_factory' - require 'rmagick' -rescue LoadError - # noop -end - namespace :gemojione do + desc 'Generates Emoji SHA256 digests' + task digests: :environment do + require 'digest/sha2' + require 'json' + + dir = Gemojione.index.images_path + + digests = AwardEmoji.emojis.map do |name, emoji_hash| + fpath = File.join(dir, "#{emoji_hash['unicode']}.png") + digest = Digest::SHA256.file(fpath).hexdigest + + { name: name, unicode: emoji_hash['unicode'], digest: digest } + end + + out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + + File.open(out, 'w') do |handle| + handle.write(JSON.pretty_generate(digests)) + end + end + + # This task will generate a standard and Retina sprite of all of the current + # Gemojione Emojis, with the accompanying SCSS map. + # + # It will not appear in `rake -T` output, and the dependent gems are not + # included in the Gemfile by default, because this task will only be needed + # occasionally, such as when new Emojis are added to Gemojione. task sprite: :environment do + begin + require 'sprite_factory' + require 'rmagick' + rescue LoadError + # noop + end + check_requirements! SIZE = 20 diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 27ed57efe55..effb8eb6001 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -623,7 +623,6 @@ namespace :gitlab do start_checking "Reply by email" if Gitlab.config.incoming_email.enabled - check_address_formatted_correctly check_imap_authentication if Rails.env.production? @@ -643,20 +642,6 @@ namespace :gitlab do # Checks ######################## - def check_address_formatted_correctly - print "Address formatted correctly? ... " - - if Gitlab::IncomingEmail.address_formatted_correctly? - puts "yes".green - else - puts "no".red - try_fixing_it( - "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder." - ) - fix_and_rerun - end - end - def check_initd_configured_correctly print "Init.d configured correctly? ... " diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 5b1f65d7aff..9ef8ba1b097 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -1,15 +1,14 @@ require 'spec_helper' describe Admin::UsersController do - let(:admin) { create(:admin) } + let(:user) { create(:user) } before do - sign_in(admin) + sign_in(create(:admin)) end describe 'DELETE #user with projects' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:empty_project, namespace: user.namespace) } before do project.team << [user, :developer] @@ -23,8 +22,6 @@ describe Admin::UsersController do end describe 'PUT block/:id' do - let(:user) { create(:user) } - it 'blocks user' do put :block, id: user.username user.reload @@ -50,8 +47,6 @@ describe Admin::UsersController do end context 'manually blocked users' do - let(:user) { create(:user) } - before do user.block end @@ -66,8 +61,6 @@ describe Admin::UsersController do end describe 'PUT unlock/:id' do - let(:user) { create(:user) } - before do request.env["HTTP_REFERER"] = "/" user.lock_access! @@ -95,8 +88,6 @@ describe Admin::UsersController do end describe 'PATCH disable_two_factor' do - let(:user) { create(:user) } - it 'disables 2FA for the user' do expect(user).to receive(:disable_two_factor!) allow(subject).to receive(:user).and_return(user) diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb index db0748f323f..5022a3e2c80 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/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c5b034dc064..75e6b6f45a7 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: format) - expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) + expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s) end it "should not escape Html" do diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb new file mode 100644 index 00000000000..0f32a30f18b --- /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/factories/file_uploader.rb b/spec/factories/file_uploader.rb new file mode 100644 index 00000000000..1b36e21f2b0 --- /dev/null +++ b/spec/factories/file_uploader.rb @@ -0,0 +1,20 @@ +FactoryGirl.define do + factory :file_uploader do + project + secret nil + + transient do + fixture { 'rails_sample.jpg' } + path { File.join(Rails.root, 'spec/fixtures', fixture) } + file { Rack::Test::UploadedFile.new(path) } + end + + after(:build) do |uploader, evaluator| + uploader.store!(evaluator.file) + end + + initialize_with do + new(project, secret) + end + end +end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 457859dedaf..62de081661d 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' -FactoryGirl.factories.map(&:name).each do |factory_name| - describe "#{factory_name} factory" do - it 'should be valid' do - expect(build(factory_name)).to be_valid +describe 'factories' do + FactoryGirl.factories.each do |factory| + describe "#{factory.name} factory" do + let(:entity) { build(factory.name) } + + it 'does not raise error when created 'do + expect { entity }.to_not raise_error + end + + it 'should be valid', if: factory.build_class < ActiveRecord::Base do + expect(entity).to be_valid + end end end end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index f6e33f651c4..99445185893 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_spec.rb b/spec/features/issues_spec.rb index e844e681ebf..db46657c36a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -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/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb new file mode 100644 index 00000000000..fd02d584848 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true, js: false do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + project.team << [user, :master] + + login_as user + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'generates a diff for an orphaned branch' do + click_link 'New Merge Request' + select "orphaned-branch", from: "merge_request_source_branch" + select "master", from: "merge_request_target_branch" + click_button "Compare branches" + + expect(page).to have_content "README.md" + expect(page).to have_content "wm.png" + + fill_in "merge_request_title", with: "Orphaned MR test" + click_button "Submit merge request" + + expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' + end +end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index b76e4c74c79..c57ab5f3b03 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/search_spec.rb b/spec/features/search_spec.rb index 84c036e59c0..3e6289a46b1 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -1,19 +1,46 @@ require 'spec_helper' describe "Search", feature: true do - 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 00000000000..db53a9cec97 --- /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 00000000000..d23d645c8e5 --- /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 00000000000..e3665b6116a --- /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 00000000000..39d5cefbc2a --- /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 1e696389954..980e10a8812 100644 --- a/spec/fixtures/emails/valid_reply.eml +++ b/spec/fixtures/emails/valid_reply.eml @@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400 From: Jake the Dog 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/lib/award_emoji_spec.rb b/spec/lib/award_emoji_spec.rb new file mode 100644 index 00000000000..330678f7f16 --- /dev/null +++ b/spec/lib/award_emoji_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe AwardEmoji do + describe '.urls' do + subject { AwardEmoji.urls } + + it { is_expected.to be_an_instance_of(Array) } + it { is_expected.to_not be_empty } + + context 'every Hash in the Array' do + it 'has the correct keys and values' do + subject.each do |hash| + expect(hash[:name]).to be_an_instance_of(String) + expect(hash[:path]).to be_an_instance_of(String) + end + end + end + end +end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index f38fadda9ba..566035c60d0 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers let(:project) { double('project') } diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb new file mode 100644 index 00000000000..b78c2b6224f --- /dev/null +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::Badge::Build do + let(:project) { create(:project) } + let(:sha) { project.commit.sha } + let(:badge) { described_class.new(project, 'master') } + + describe '#type' do + subject { badge.type } + it { is_expected.to eq 'image/svg+xml' } + end + + context 'build exists' do + let(:ci_commit) { create(:ci_commit, project: project, sha: sha) } + let!(:build) { create(:ci_build, commit: ci_commit) } + + + context 'build success' do + before { build.success! } + + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-success' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about success' do + expect(status_node(data, 'success')).to be_truthy + end + end + end + + context 'build failed' do + before { build.drop! } + + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-failed' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about failure' do + expect(status_node(data, 'failed')).to be_truthy + end + end + end + end + + context 'build does not exist' do + describe '#to_s' do + subject { badge.to_s } + it { is_expected.to eq 'build-unknown' } + end + + describe '#data' do + let(:data) { badge.data } + + it 'contains infromation about unknown build' do + expect(status_node(data, 'unknown')).to be_truthy + end + end + end + + def status_node(data, status) + xml = Nokogiri::XML.parse(data) + xml.at(%Q{text:contains("#{status}")}) + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 844fd79c991..a1f51429a79 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -236,6 +236,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index abe179cd4af..36267faeb93 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe Gitlab::Email::Receiver, lib: true do before do stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') end let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } @@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do expect(note.note).to include(markdown) end + + context 'when sub-addressing is not supported' do + before do + stub_incoming_email_setting(enabled: true, address: nil) + end + + shared_examples 'an email that contains a reply key' do |header| + it "fetches the reply key from the #{header} header and creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include('I could not disagree more.') + end + end + + context 'reply key is in the References header' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } + + it_behaves_like 'an email that contains a reply key', 'References' + end + end end end diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb new file mode 100644 index 00000000000..2dc71be0254 --- /dev/null +++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::FogbugzImport::Client, lib: true do + + let(:client) { described_class.new(uri: '', token: '') } + let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } } + let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } } + + it 'retrieves user_map with one user' do + stub_api(one_user) + + expect(client.user_map.count).to eq(1) + end + + it 'retrieves user_map with two users' do + stub_api(two_users) + + expect(client.user_map.count).to eq(2) + end + + def stub_api(users) + allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users) + end +end diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb new file mode 100644 index 00000000000..eda956e6f0a --- /dev/null +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::Gfm::UploadsRewriter do + let(:user) { create(:user) } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + let(:rewriter) { described_class.new(text, old_project, user) } + + context 'text contains links to uploads' do + let(:image_uploader) do + build(:file_uploader, project: old_project) + end + + let(:zip_uploader) do + build(:file_uploader, project: old_project, + fixture: 'ci_build_artifacts.zip') + end + + let(:text) do + "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}" + end + + describe '#rewrite' do + let!(:new_text) { rewriter.rewrite(new_project) } + + let(:old_files) { [image_uploader, zip_uploader].map(&:file) } + let(:new_files) do + described_class.new(new_text, new_project, user).files + end + + let(:old_paths) { old_files.map(&:path) } + let(:new_paths) { new_files.map(&:path) } + + it 'rewrites content' do + expect(new_text).to_not eq text + expect(new_text.length).to eq text.length + end + + it 'copies files' do + expect(new_files).to all(exist) + expect(old_paths).to_not match_array new_paths + expect(old_paths).to all(include(old_project.path_with_namespace)) + expect(new_paths).to all(include(new_project.path_with_namespace)) + end + + it 'does not remove old files' do + expect(old_files).to all(exist) + end + + it 'generates a new secret for each file' do + expect(new_paths).to_not include image_uploader.secret + expect(new_paths).to_not include zip_uploader.secret + end + end + + describe '#needs_rewrite?' do + subject { rewriter.needs_rewrite? } + it { is_expected.to eq true } + end + + describe '#files' do + subject { rewriter.files } + it { is_expected.to be_an(Array) } + end + end +end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index bcdba8d4c12..afb3e26f8fb 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -7,24 +7,8 @@ describe Gitlab::IncomingEmail, lib: true do stub_incoming_email_setting(enabled: true) end - context "when the address is valid" do - before do - stub_incoming_email_setting(address: "replies+%{key}@example.com") - end - - it "returns true" do - expect(described_class.enabled?).to be_truthy - end - end - - context "when the address is invalid" do - before do - stub_incoming_email_setting(address: "replies@example.com") - end - - it "returns false" do - expect(described_class.enabled?).to be_falsey - end + it 'returns true' do + expect(described_class.enabled?).to be_truthy end end @@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do expect(described_class.key_from_address("replies+key@example.com")).to eq("key") end end + + context 'self.key_from_fallback_reply_message_id' do + it 'returns reply key' do + expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key') + end + end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 9b47acfe0cd..631b5094f42 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -35,7 +35,9 @@ describe Notify do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } it_behaves_like 'an assignee email' - it_behaves_like 'an email starting a new thread', 'issue' + it_behaves_like 'an email starting a new thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -73,9 +75,11 @@ describe Notify do subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -104,7 +108,9 @@ describe Notify do subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'an email with a labels subscriptions link in its footer' @@ -132,7 +138,9 @@ describe Notify do let(:status) { 'closed' } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -163,7 +171,9 @@ describe Notify do let(:new_issue) { create(:issue) } subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' @@ -196,9 +206,11 @@ describe Notify do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } it_behaves_like 'an assignee email' - it_behaves_like 'an email starting a new thread', 'merge_request' + it_behaves_like 'an email starting a new thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -216,10 +228,6 @@ describe Notify do is_expected.to have_body_text /#{merge_request.target_branch}/ end - it 'has the correct message-id set' do - is_expected.to have_header 'Message-ID', "" - end - context 'when enabled email_author_in_body' do before do allow(current_application_settings).to receive(:email_author_in_body).and_return(true) @@ -247,7 +255,9 @@ describe Notify do subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like "an unsubscribeable thread" @@ -278,7 +288,9 @@ describe Notify do subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'an email with a labels subscriptions link in its footer' @@ -306,9 +318,11 @@ describe Notify do let(:status) { 'reopened' } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -337,9 +351,11 @@ describe Notify do subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) } it_behaves_like 'a multiple recipients email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' - it_behaves_like "an unsubscribeable thread" + it_behaves_like 'an unsubscribeable thread' it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -456,9 +472,11 @@ describe Notify do subject { Notify.note_commit_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'commit' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { commit } + end it_behaves_like 'it should show Gmail Actions View Commit link' - it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'a user cannot unsubscribe through footer link' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -477,7 +495,9 @@ describe Notify do subject { Notify.note_merge_request_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'an unsubscribeable thread' @@ -498,7 +518,9 @@ describe Notify do subject { Notify.note_issue_email(recipient.id, note.id) } it_behaves_like 'a note email' - it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'an unsubscribeable thread' diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 6019af544d3..56a6dbf96f9 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 diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b1764d7ac09..520cf1b75de 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index be29b6d66ff..b16ccc6e305 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -9,6 +9,7 @@ describe Issue, "Issuable" do it { is_expected.to belong_to(:author) } it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe "Validation" do diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index fd1513cab1b..56a9fbe9720 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -20,24 +20,27 @@ require "spec_helper" describe SystemHook, models: true do describe "execute" do - before(:each) do - @system_hook = create(:system_hook) - WebMock.stub_request(:post, @system_hook.url) + let(:system_hook) { create(:system_hook) } + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:group) { create(:group) } + + before do + WebMock.stub_request(:post, system_hook.url) end it "project_create hook" do - Projects::CreateService.new(create(:user), name: 'empty').execute - expect(WebMock).to have_requested(:post, @system_hook.url).with( + Projects::CreateService.new(user, name: 'empty').execute + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_create/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it "project_destroy hook" do - user = create(:user) - project = create(:empty_project, namespace: user.namespace) Projects::DestroyService.new(project, user, {}).pending_delete! - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once @@ -45,37 +48,36 @@ describe SystemHook, models: true do it "user_create hook" do create(:user) - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it "user_destroy hook" do - user = create(:user) user.destroy - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it "project_create hook" do - user = create(:user) - project = create(:project) project.team << [user, :master] - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it "project_destroy hook" do - user = create(:user) - project = create(:project) project.team << [user, :master] project.project_members.destroy_all - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once @@ -83,41 +85,39 @@ describe SystemHook, models: true do it 'group create hook' do create(:group) - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_create/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it 'group destroy hook' do - group = create(:group) group.destroy - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it 'group member create hook' do - group = create(:group) - user = create(:user) group.add_master(user) - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_group/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end it 'group member destroy hook' do - group = create(:group) - user = create(:user) group.add_master(user) group.group_members.destroy_all - expect(WebMock).to have_requested(:post, @system_hook.url).with( + + expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_group/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end - end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bd0a4ebe337..6f5d912fe5d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -224,22 +224,22 @@ describe MergeRequest, models: true do ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix| it "detects the '#{wip_prefix}' prefix" do subject.title = "#{wip_prefix}#{subject.title}" - expect(subject).to be_work_in_progress + expect(subject.work_in_progress?).to eq true end end it "doesn't detect WIP for words starting with WIP" do subject.title = "Wipwap #{subject.title}" - expect(subject).not_to be_work_in_progress + expect(subject.work_in_progress?).to eq false end it "doesn't detect WIP for words containing with WIP" do subject.title = "WupWipwap #{subject.title}" - expect(subject).not_to be_work_in_progress + expect(subject.work_in_progress?).to eq false end it "doesn't detect WIP by default" do - expect(subject).not_to be_work_in_progress + expect(subject.work_in_progress?).to eq false end end diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index 97e6f03e308..f648cbe2dee 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -27,6 +27,16 @@ describe SlackService::IssueMessage, models: true do let(:color) { '#345' } + context '#initialize' do + before do + args[:object_attributes][:description] = nil + end + + it 'returns a non-null description' do + expect(subject.description).to eq('') + end + end + context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 55f1c665b86..f29c389e094 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -104,6 +104,15 @@ describe Project, models: true do end end + describe 'default_scope' do + it 'excludes projects pending deletion from the results' do + project = create(:empty_project) + create(:empty_project, pending_delete: true) + + expect(Project.all).to eq [project] + end + end + describe 'project token' do it 'should set an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f10d671104c..c5d5a1c2492 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -303,7 +303,7 @@ describe Repository, models: true do describe 'when there are no branches' do before do - allow(repository.raw_repository).to receive(:branch_count).and_return(0) + allow(repository).to receive(:branch_count).and_return(0) end it { is_expected.to eq(false) } @@ -311,13 +311,13 @@ describe Repository, models: true do describe 'when there are branches' do it 'returns true' do - expect(repository.raw_repository).to receive(:branch_count).and_return(3) + expect(repository).to receive(:branch_count).and_return(3) expect(subject).to eq(true) end it 'caches the output' do - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(3) @@ -436,7 +436,7 @@ describe Repository, models: true do it 'expires the visible content cache' do repository.has_visible_content? - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(0) @@ -558,7 +558,7 @@ describe Repository, models: true do end it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + expect(repository).to receive(:expire_exists_cache).twice repository.before_delete end @@ -865,4 +865,21 @@ describe Repository, models: true do repository.build_cache end end + + describe '#local_branches' do + it 'returns the local branches' do + masterrev = repository.find_branch('master').target + create_remote_branch('joe', 'remote_branch', masterrev) + repository.add_branch(user, 'local_branch', masterrev) + + expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) + expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + end + end + + def create_remote_branch(remote_name, branch_name, target) + rugged = repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) + end + end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0ab7fd88ce6..8b2fb77e28e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -173,6 +173,13 @@ describe User, models: true do expect(user).to be_invalid end end + + context 'owns_notification_email' do + it 'accepts temp_oauth_email emails' do + user = build(:user, email: "temp-email-for-oauth@example.com") + expect(user).to be_valid + end + end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index ce55cb7b0ae..822d3ad3017 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -318,6 +318,17 @@ describe API::API, api: true do 'is too long (maximum is 255 characters)' ]) end + + context 'when an admin or owner makes the request' do + it "accepts the creation date to be set" do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago + + expect(response.status).to eq(201) + # this take about a second, so probably not equal + expect(Time.parse(json_response['created_at'])).to be <= 2.weeks.ago + end + end end describe 'POST /projects/:id/issues with spam filtering' do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 667f0dbea5c..6943ff9d26c 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -23,13 +23,25 @@ describe API::API, api: true do end describe 'POST /projects/:id/labels' do - it 'should return created label' do + it 'should return created label when all params' do + post api("/projects/#{project.id}/labels", user), + name: 'Foo', + color: '#FFAABB', + description: 'test' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('Foo') + expect(json_response['color']).to eq('#FFAABB') + expect(json_response['description']).to eq('test') + end + + it 'should return created label when only required params' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAABB' expect(response.status).to eq(201) expect(json_response['name']).to eq('Foo') expect(json_response['color']).to eq('#FFAABB') + expect(json_response['description']).to be_nil end it 'should return a 400 bad request if name not given' do @@ -94,14 +106,16 @@ describe API::API, api: true do end describe 'PUT /projects/:id/labels' do - it 'should return 200 if name and colors are changed' do + it 'should return 200 if name and colors and description are changed' do put api("/projects/#{project.id}/labels", user), name: 'label1', new_name: 'New Label', - color: '#FFFFFF' + color: '#FFFFFF', + description: 'test' expect(response.status).to eq(200) expect(json_response['name']).to eq('New Label') expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['description']).to eq('test') end it 'should return 200 if name is changed' do @@ -122,6 +136,15 @@ describe API::API, api: true do expect(json_response['color']).to eq('#FFFFFF') end + it 'should return 200 if description is changed' do + put api("/projects/#{project.id}/labels", user), + name: 'label1', + description: 'test' + expect(response.status).to eq(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['description']).to eq('test') + end + it 'should return 404 if label does not exist' do put api("/projects/#{project.id}/labels", user), name: 'label2', diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c9175a4d6eb..25fa30b2f21 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -118,6 +118,7 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['work_in_progress']).to eq(false) expect(json_response['merge_status']).to eq('can_be_merged') end @@ -133,6 +134,16 @@ describe API::API, api: true do get api("/projects/#{project.id}/merge_requests/999", user) expect(response.status).to eq(404) end + + context 'Work in Progress' do + let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } + + it "should return merge_request" do + get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) + expect(response.status).to eq(200) + expect(json_response['work_in_progress']).to eq(true) + end + end end describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a5d4985dc78..be2034e0f39 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -948,6 +948,78 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/archive' do + context 'on an unarchived project' do + it 'archives the project' do + post api("/projects/#{project.id}/archive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'remains archived' do + post api("/projects/#{project.id}/archive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post api("/projects/#{project.id}/archive", user3) + + expect(response.status).to eq(403) + end + end + end + + describe 'POST /projects/:id/unarchive' do + context 'on an unarchived project' do + it 'remains unarchived' do + post api("/projects/#{project.id}/unarchive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'unarchives the project' do + post api("/projects/#{project.id}/unarchive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post api("/projects/#{project.id}/unarchive", user3) + + expect(response.status).to eq(403) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 9b0c73aaf37..2a5e4ac3ec4 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -160,6 +160,20 @@ describe Issues::MoveService, services: true do .to eq "Note with reference to merge request #{old_project.to_reference}!1" end end + + context 'issue description with uploads' do + let(:uploader) { build(:file_uploader, project: old_project) } + let(:description) { "Text and #{uploader.to_markdown}" } + + include_context 'issue move executed' + + it 'rewrites uploads in description' do + expect(new_issue.description).to_not eq description + expect(new_issue.description) + .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/) + expect(new_issue.description).to_not include uploader.secret + end + end end describe 'rewritting references' do diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb new file mode 100644 index 00000000000..23f5555d3e0 --- /dev/null +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::UnlinkForkService, services: true do + subject { Projects::UnlinkForkService.new(fork_project, user) } + + let(:fork_link) { create(:forked_project_link) } + let(:fork_project) { fork_link.forked_to_project } + let(:user) { create(:user) } + + context 'with opened merge request on the source project' do + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + + before do + allow(MergeRequests::CloseService).to receive(:new). + with(fork_project, user). + and_return(mr_close_service) + end + + it 'close all pending merge requests' do + expect(mr_close_service).to receive(:execute).with(merge_request) + + subject.execute + end + end + + it 'remove fork relation' do + expect(fork_project.forked_project_link).to receive(:destroy) + + subject.execute + end +end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index b4728807b8b..82b7fbfa816 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -2,22 +2,25 @@ require 'spec_helper' describe TodoService, services: true do let(:author) { create(:user) } - let(:john_doe) { create(:user, username: 'john_doe') } - let(:michael) { create(:user, username: 'michael') } - let(:stranger) { create(:user, username: 'stranger') } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:john_doe) { create(:user) } let(:project) { create(:project) } - let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') } + let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') } let(:service) { described_class.new } before do project.team << [author, :developer] + project.team << [member, :developer] project.team << [john_doe, :developer] - project.team << [michael, :developer] end describe 'Issues' do let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } describe '#new_issue' do it 'creates a todo if assigned' do @@ -37,10 +40,20 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_issue(issue, author) - should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + end + + it 'does not create todo for non project members when issue is confidential' do + service.new_issue(confidential_issue, john_doe) + + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED) + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end end @@ -48,16 +61,26 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.update_issue(issue, author) - should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end it 'does not create a todo if user was already mentioned' do - create(:todo, :mentioned, user: michael, project: project, target: issue, author: author) + create(:todo, :mentioned, user: member, project: project, target: issue, author: author) - expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count) + expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) + end + + it 'does not create todo for non project members when issue is confidential' do + service.update_issue(confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end end @@ -109,8 +132,10 @@ describe TodoService, services: true do describe '#new_note' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } let(:system_note) { create(:system_note, project: project, noteable: issue) } @@ -142,19 +167,29 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_note(note, john_doe) - should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) - should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'does not create todo for non project members when leaving a note on a confidential issue' do + service.new_note(note_on_confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end it 'creates a todo for each valid mentioned user when leaving a note on commit' do service.new_note(note_on_commit, john_doe) - should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end it 'does not create todo when leaving a note on snippet' do @@ -185,10 +220,10 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_merge_request(mr_assigned, author) - should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end end @@ -196,16 +231,16 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.update_merge_request(mr_assigned, author) - should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end it 'does not create a todo if user was already mentioned' do - create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author) + create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author) - expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count) + expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end end diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb new file mode 100644 index 00000000000..aa89afd8fb3 --- /dev/null +++ b/spec/support/carrierwave.rb @@ -0,0 +1,7 @@ +CarrierWave.root = 'tmp/tests/uploads' + +RSpec.configure do |config| + config.after(:suite) do + FileUtils.rm_rf('tmp/tests/uploads') + end +end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index ef5ea7d626e..e849a9633b9 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -78,6 +78,6 @@ module FilterSpecHelper # Shortcut to Rails' auto-generated routes helpers, to avoid including the # module def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 73c6792b65f..b87cd6bbca2 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -106,7 +106,7 @@ class MarkdownFeature end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def raw_markdown diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 0d1bd030f3c..71664bb192e 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -15,6 +15,7 @@ module TestEnv 'lfs' => 'be93687', 'master' => '5937ac0', "'test'" => 'e56497b', + 'orphaned-branch' => '45127a9', } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 320be9a0b61..05fc4c4554f 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -7,8 +7,12 @@ describe 'gitlab:app namespace rake task' do Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/db' + # empty task as env is already loaded Rake::Task.define_task :environment + + # We need this directory to run `gitlab:backup:create` task + FileUtils.mkdir_p('public/uploads') end def run_rake_task(task_name) diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index b11c5de94e3..1abd87d7d33 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -22,6 +22,8 @@ describe MergeWorker do merge_request.reload expect(merge_request).to be_merged + + source_project.repository.expire_branches_cache expect(source_project.repository.branch_names).not_to include('markdown') end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb new file mode 100644 index 00000000000..7e59bd2fced --- /dev/null +++ b/spec/workers/project_cache_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe ProjectCacheWorker do + let(:project) { create(:project) } + + subject { described_class.new } + + describe '#perform' do + it 'updates project cache data' do + + expect_any_instance_of(Repository).to receive(:size) + expect_any_instance_of(Repository).to receive(:commit_count) + + expect_any_instance_of(Project).to receive(:update_repository_size) + expect_any_instance_of(Project).to receive(:update_commit_count) + + subject.perform(project.id) + end + + it 'handles missing repository data' do + expect_any_instance_of(Repository).to receive(:exists?).and_return(false) + expect_any_instance_of(Repository).not_to receive(:size) + + subject.perform(project.id) + end + end +end diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css new file mode 100644 index 00000000000..b6f61295392 --- /dev/null +++ b/vendor/assets/stylesheets/animate.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +/*! + * animate.css -http://daneden.me/animate + * Version - 3.5.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2016 Daniel Eden + */ + +.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file