diff --git a/.rubocop.yml b/.rubocop.yml index 71273ce609..2fda0b0311 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -691,7 +691,7 @@ Style/ZeroLengthPredicate: # branches, and conditions. Metrics/AbcSize: Enabled: true - Max: 70 + Max: 60 # Avoid excessive block nesting. Metrics/BlockNesting: diff --git a/CHANGELOG b/CHANGELOG index cfe47a4e56..db86f55c9d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,31 +1,58 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse) + - Fix revoking of authorized OAuth applications (Connor Shea) + - All service classes (those residing in app/services) are now instrumented (Yorick Peterse) + - Developers can now add custom tags to transactions (Yorick Peterse) + - Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse) + - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) + - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) + - Add setting for customizing the list of trusted proxies !3524 + - Allow projects to be transfered to a lower visibility level group + - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - 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) + - API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling) - Expose project badges in project settings - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - - Allow back dating on issues when created through the API + - API: Ability to update a group (Robert Schilling) + - API: Ability to move issues (Robert Schilling) - Fix Error 500 after renaming a project path (Stan Hu) + - Fix a bug whith trailing slash in teamcity_url (Charles May) + - Allow back dating on issues when created or updated through the API + - Allow back dating on issue notes when created through the API - Fix avatar stretching by providing a cropping feature + - API: Expose `subscribed` for issues and merge requests (Robert Schilling) - Allow SAML to handle external users based on user's information !3530 + - Allow Omniauth providers to be marked as `external` !3657 - Add endpoints to archive or unarchive a project !3372 + - Fix a bug whith trailing slash in bamboo_url - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) + - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) + - API: Ability to star and unstar a project (Robert Schilling) - Add default scope to projects to exclude projects pending deletion + - Allow to close merge requests which source projects(forks) are deleted. - Ensure empty recipients are rejected in BuildsEmailService - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) + - API: Fix milestone filtering by `iid` (Robert Schilling) + - API: Delete notes of issues, snippets, and merge requests (Robert Schilling) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) + - Better errors handling when creating milestones inside groups + - Fix high CPU usage when PostReceive receives refs/merge-requests/ + - Hide `Create a group` help block when creating a new project in a group - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Decouple membership and notifications - Fix creation of merge requests for orphaned branches (Stan Hu) + - API: Ability to retrieve a single tag (Robert Schilling) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - - Improved UX of the navigation sidebar - Fix admin/projects when using visibility levels on search (PotHix) - Build status notifications - API: Expose user location (Robert Schilling) @@ -34,9 +61,33 @@ v 8.7.0 (unreleased) v 8.6.5 (unreleased) - Only update repository language if it is not set to improve performance - Check permissions when user attempts to import members from another project + - API: Do not leak group existence via return code (Robert Schilling) + - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 + - Update number of Todos in the sidebar when it's marked as "Done". !3600 + - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) + - API: User can leave a project through the API when not master or owner. !3613 + - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) + - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) + - Improved markdown forms + - Diffs load at the correct point when linking from from number + - Selected diff rows highlight + +v 8.6.6 + - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) + - Project switcher uses new dropdown styling + +v 8.6.5 + - Fix importing from GitHub Enterprise. !3529 + - Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533 + - Check permissions when user attempts to import members from another project. !3535 + - Only update repository language if it is not set to improve performance. !3556 + - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu). !3583 + - Unblock user when active_directory is disabled and it can be found !3550 + - Fix a 2FA authentication spoofing vulnerability. v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) + - Redesign the Labels page v 8.6.3 - Mentions on confidential issues doesn't create todos for non-members. !3374 @@ -153,6 +204,9 @@ v 8.6.0 - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests +v 8.5.10 + - Fix a 2FA authentication spoofing vulnerability. + v 8.5.9 - Don't attempt to fetch any tags from a forked repo (Stan Hu). @@ -297,6 +351,9 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.8 + - Fix a 2FA authentication spoofing vulnerability. + v 8.4.7 - Don't attempt to fetch any tags from a forked repo (Stan Hu). @@ -416,6 +473,9 @@ v 8.4.0 - Add IP check against DNSBLs at account sign-up - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching +v 8.3.7 + - Fix a 2FA authentication spoofing vulnerability. + v 8.3.6 - Don't attempt to fetch any tags from a forked repo (Stan Hu). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 511336f384..1f26a5d7ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -448,7 +448,7 @@ merge request: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) -1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) +1. [Testing](doc/development/testing.md) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [SCSS styleguide][scss-styleguide] 1. [Shell commands](doc/development/shell_commands.md) created by GitLab diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 24ba9a38de..37c2961c24 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.7.0 +2.7.2 diff --git a/Gemfile b/Gemfile index 0279b4ac47..199ef65d92 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.5.2' +gem 'rails', '4.2.6' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with @@ -8,7 +8,7 @@ gem 'responders', '~> 2.0' # Specify a sprockets version due to increased performance # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 -gem 'sprockets', '~> 3.3.5' +gem 'sprockets', '~> 3.6.0' # Default values for AR models gem "default_value_for", "~> 3.0.0" @@ -149,6 +149,10 @@ gem 'version_sorter', '~> 2.0.0' # Cache gem "redis-rails", '~> 4.0.0' +# Redis +gem 'redis', '~> 3.2' +gem 'connection_pool', '~> 2.0' + # Campfire integration gem 'tinder', '~> 1.10.0' @@ -229,14 +233,13 @@ group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'method_source', '~> 0.8', require: false gem 'influxdb', '~> 0.2', require: false - gem 'connection_pool', '~> 2.0', require: false end group :development do gem "foreman" gem 'brakeman', '~> 3.2.0', require: false - gem "annotate", "~> 2.6.0" + gem "annotate", "~> 2.7.0" gem "letter_opener", '~> 1.1.2' gem 'quiet_assets', '~> 1.0.2' gem 'rerun', '~> 0.11.0' @@ -282,9 +285,9 @@ group :development, :test do gem 'teaspoon', '~> 1.1.0' gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.6.4' + gem 'spring', '~> 1.7.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.0.0' + gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.38.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 1ba8d748db..ad7d7c1855 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) + actionmailer (4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5.2) - actionview (= 4.2.5.2) - activesupport (= 4.2.5.2) + actionpack (4.2.6) + actionview (= 4.2.6) + activesupport (= 4.2.6) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5.2) - activesupport (= 4.2.5.2) + actionview (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5.2) - activesupport (= 4.2.5.2) + activejob (4.2.6) + activesupport (= 4.2.6) globalid (>= 0.3.0) - activemodel (4.2.5.2) - activesupport (= 4.2.5.2) + activemodel (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) - activerecord (4.2.5.2) - activemodel (= 4.2.5.2) - activesupport (= 4.2.5.2) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.5.2) + activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -51,8 +51,8 @@ GEM activerecord (>= 3.0) akismet (2.0.0) allocations (1.0.4) - annotate (2.6.10) - activerecord (>= 3.2, <= 4.3) + annotate (2.7.0) + activerecord (>= 3.2, < 6.0) rake (~> 10.4) arel (6.0.3) asana (0.4.0) @@ -145,7 +145,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) - css_parser (1.3.7) + css_parser (1.4.1) addressable d3_rails (3.5.11) railties (>= 3.1.0) @@ -459,8 +459,8 @@ GEM nokogiri (>= 1.5.9) macaddr (1.7.1) systemu (~> 2.6.2) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) mail_room (0.6.1) method_source (0.8.2) mime-types (1.25.1) @@ -559,8 +559,8 @@ GEM premailer (1.8.6) css_parser (>= 1.3.6) htmlentities (>= 4.0.0) - premailer-rails (1.9.0) - actionmailer (>= 3, < 5) + premailer-rails (1.9.2) + actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) pry (0.10.3) coderay (~> 1.1.0) @@ -589,16 +589,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.5.2) - actionmailer (= 4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) - activemodel (= 4.2.5.2) - activerecord (= 4.2.5.2) - activesupport (= 4.2.5.2) + rails (4.2.6) + actionmailer (= 4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + activemodel (= 4.2.6) + activerecord (= 4.2.6) + activesupport (= 4.2.6) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5.2) + railties (= 4.2.6) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -608,9 +608,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.5.2) - actionpack (= 4.2.5.2) - activesupport (= 4.2.5.2) + railties (4.2.6) + actionpack (= 4.2.6) + activesupport (= 4.2.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) @@ -769,19 +769,20 @@ GEM spinach (>= 0.4) spinach-rerun-reporter (0.0.2) spinach (~> 0.8) - spring (1.6.4) + spring (1.7.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.0.0) + spring-commands-spinach (1.1.0) spring (>= 0.9.1) spring-commands-teaspoon (0.0.2) spring (>= 0.9.1) - sprockets (3.3.5) + sprockets (3.6.0) + concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) state_machines (0.4.0) state_machines-activemodel (0.3.0) activemodel (~> 4.1) @@ -887,7 +888,7 @@ DEPENDENCIES after_commit_queue akismet (~> 2.0) allocations (~> 1.0) - annotate (~> 2.6.0) + annotate (~> 2.7.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) @@ -992,13 +993,14 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.5.2) + rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) recaptcha redcarpet (~> 3.3.3) + redis (~> 3.2) redis-namespace redis-rails (~> 4.0.0) request_store (~> 1.3.0) @@ -1028,11 +1030,11 @@ DEPENDENCIES slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) spinach-rerun-reporter (~> 0.0.2) - spring (~> 1.6.4) + spring (~> 1.7.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.0.0) + spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) - sprockets (~> 3.3.5) + sprockets (~> 3.6.0) state_machines-activerecord (~> 0.3.0) task_list (~> 1.0.2) teaspoon (~> 1.1.0) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index f01c67e947..6f435e4c54 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -22,7 +22,17 @@ #= require cal-heatmap #= require turbolinks #= require autosave -#= require bootstrap +#= require bootstrap/affix +#= require bootstrap/alert +#= require bootstrap/button +#= require bootstrap/collapse +#= require bootstrap/dropdown +#= require bootstrap/modal +#= require bootstrap/scrollspy +#= require bootstrap/tab +#= require bootstrap/transition +#= require bootstrap/tooltip +#= require bootstrap/popover #= require select2 #= require raphael #= require g.raphael @@ -41,6 +51,7 @@ #= require shortcuts_issuable #= require shortcuts_network #= require jquery.nicescroll +#= require date.format #= require_tree . #= require fuzzaldrin-plus #= require cropper @@ -163,7 +174,7 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $('abbr.timeago, .js-timeago').timeago() + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false) # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 6a670d5e88..af4462ece3 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -22,8 +22,19 @@ class @AwardsHandler emoji = $(this) .find(".icon") .data "emoji" + + if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown" + awards_handler.addAward "thumbsdown" + + else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup" + awards_handler.addAward "thumbsup" + awards_handler.addAward emoji + didUserClickEmoji: (that, emoji) -> + if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") + $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 + showEmojiMenu: -> if $(".emoji-menu").length if $(".emoji-menu").is ".is-visible" @@ -105,7 +116,7 @@ class @AwardsHandler if origTitle authors = origTitle.split(', ') authors.push("me") - award_block.attr("title", authors.join(", ")) + award_block.attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> @@ -122,7 +133,7 @@ class @AwardsHandler nodes = [] nodes.push( - "" diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 6e29d37426..3cb96bacaa 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> e.preventDefault() $form = $(e.target).closest('form') - $form.find('input[type=submit], button[type=submit]').disable() + $submit_button = $form.find('input[type=submit], button[type=submit]') + + return if $submit_button.attr('disabled') + + $submit_button.disable() $form.submit() # If the user tabs to a submit button on a `js-quick-submit` form, display a diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee index 79d750d184..0faa570ce1 100644 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -35,4 +35,18 @@ $.fn.requiresInput = -> $form.on 'change input', fieldSelector, requireInput $ -> - $('form.js-requires-input').requiresInput() + $form = $('form.js-requires-input') + $form.requiresInput() + + # Hide or Show the help block when creating a new project + # based on the option selected + hideOrShowHelpBlock = (form) -> + selected = $('.js-select-namespace option:selected') + if selected.length and selected.data('options-parent') is 'groups' + return form.find('.help-block').hide() + else if selected.length + form.find('.help-block').show() + + hideOrShowHelpBlock($form) + + $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form) diff --git a/app/assets/javascripts/compare.js.coffee b/app/assets/javascripts/compare.js.coffee new file mode 100644 index 0000000000..f20992ead3 --- /dev/null +++ b/app/assets/javascripts/compare.js.coffee @@ -0,0 +1,67 @@ +class @Compare + constructor: (@opts) -> + @source_loading = $ ".js-source-loading" + @target_loading = $ ".js-target-loading" + + $('.js-compare-dropdown').each (i, dropdown) => + $dropdown = $(dropdown) + + $dropdown.glDropdown( + selectable: true + fieldName: $dropdown.data 'field-name' + filterable: true + id: (obj, $el) -> + $el.data 'id' + toggleLabel: (obj, $el) -> + $el.text().trim() + clicked: (e, el) => + if $dropdown.is '.js-target-branch' + @getTargetHtml() + else if $dropdown.is '.js-source-branch' + @getSourceHtml() + else if $dropdown.is '.js-target-project' + @getTargetProject() + ) + + @initialState() + + initialState: -> + @getSourceHtml() + @getTargetHtml() + + getTargetProject: -> + $.ajax( + url: @opts.targetProjectUrl + data: + target_project_id: $("input[name='merge_request[target_project_id]']").val() + beforeSend: -> + $('.mr_target_commit').empty() + success: (html) -> + $('.js-target-branch-dropdown .dropdown-content').html html + ) + + getSourceHtml: -> + @sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit', + ref: $("input[name='merge_request[source_branch]']").val() + ) + + getTargetHtml: -> + @sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit', + target_project_id: $("input[name='merge_request[target_project_id]']").val() + ref: $("input[name='merge_request[target_branch]']").val() + ) + + sendAjax: (url, loading, target, data) -> + $target = $(target) + + $.ajax( + url: url + data: data + beforeSend: -> + loading.show() + $target.empty() + success: (html) -> + loading.hide() + $target.html html + $('.js-timeago', $target).timeago() + ) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 70fd6f50e9..0b9110d35f 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -28,26 +28,26 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() - new DropzoneInput($('.milestone-form')) + new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.issue-form')) + new GLForm($('.issue-form')) new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' new Diff() shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.merge-request-form')) + new GLForm($('.merge-request-form')) new IssuableForm($('.merge-request-form')) when 'projects:tags:new' new ZenMode() - new DropzoneInput($('.tag-form')) + new GLForm($('.tag-form')) when 'projects:releases:edit' new ZenMode() - new DropzoneInput($('.release-form')) + new GLForm($('.release-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssuable(true) @@ -137,7 +137,7 @@ class Dispatcher new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() - new DropzoneInput($('.wiki-form')) + new GLForm($('.wiki-form')) when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index b502131a99..6eb8d27ee2 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -15,11 +15,13 @@ class @DropzoneInput project_uploads_path = window.project_uploads_path or null max_file_size = gon.max_file_size or 10 - form_textarea = $(form).find("textarea.markdown-area") + form_textarea = $(form).find(".js-gfm-input") form_textarea.wrap "
" form_textarea.on 'paste', (event) => handlePaste(event) + $mdArea = $(form_textarea).closest('.md-area') + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') @@ -49,17 +51,16 @@ class @DropzoneInput $(".div-dropzone-alert").alert "close" dragover: -> - form_textarea.addClass "div-dropzone-focus" + $mdArea.addClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0.7 return dragleave: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 return drop: -> - form_textarea.removeClass "div-dropzone-focus" form.find(".div-dropzone-hover").css "opacity", 0 form_textarea.focus() return diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index e8d25591f6..2dc37257e2 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -57,14 +57,30 @@ class GitLabDropdownFilter filter: (search_text) -> data = @options.data() - results = data - if search_text isnt "" - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) + if data? + results = data - @options.callback results + if search_text isnt '' + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + + @options.callback results + else + elements = @options.elements() + + if search_text + elements.each -> + $el = $(@) + matches = fuzzaldrinPlus.match($el.text().trim(), search_text) + + if matches.length + $el.show() + else + $el.hide() + else + elements.show() class GitLabDropdownRemote constructor: (@dataEndpoint, @options) -> @@ -106,7 +122,9 @@ class GitLabDropdown FILTER_INPUT = '.dropdown-input .dropdown-input-field' constructor: (@el, @options) -> - @dropdown = $(@el).parent() + self = @ + selector = $(@el).data "target" + @dropdown = if selector? then $(selector) else $(@el).parent() # Set Defaults { @@ -123,7 +141,7 @@ class GitLabDropdown if _.isString(@filterInput) @filterInput = @getElement(@filterInput) - search_fields = if @options.search then @options.search.fields else []; + searchFields = if @options.search then @options.search.fields else []; if @options.data # If data is an array @@ -147,7 +165,14 @@ class GitLabDropdown filterInputBlur: @filterInputBlur remote: @options.filterRemote query: @options.data - keys: @options.search.fields + keys: searchFields + elements: => + selector = '.dropdown-content li:not(.divider)' + + if @dropdown.find('.dropdown-toggle-page').length + selector = ".dropdown-page-one #{selector}" + + return $(selector) data: => return @fullData callback: (data) => @@ -376,7 +401,7 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) if value? if !field.length and fieldName # Create hidden input for form diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee new file mode 100644 index 0000000000..d540cc4dc4 --- /dev/null +++ b/app/assets/javascripts/gl_form.js.coffee @@ -0,0 +1,51 @@ +class @GLForm + constructor: (@form) -> + @textarea = @form.find('textarea.js-gfm-input') + + # Before we start, we should clean up any previous data for this form + @destroy() + + # Setup the form + @setupForm() + + @form.data 'gl-form', @ + + destroy: -> + # Clean form listeners + @clearEventListeners() + @form.data 'gl-form', null + + setupForm: -> + isNewForm = @form.is(':not(.gfm-form)') + + @form.removeClass 'js-new-note-form' + + if isNewForm + @form.find('.div-dropzone').remove() + @form.addClass('gfm-form') + disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') + + # remove notify commit author checkbox for non-commit notes + GitLab.GfmAutoComplete.setup() + new DropzoneInput(@form) + + autosize(@textarea) + + # form and textarea event listeners + @addEventListeners() + + # hide discard button + @form.find('.js-note-discard').hide() + + @form.show() + + clearEventListeners: -> + @textarea.off 'focus' + @textarea.off 'blur' + + addEventListeners: -> + @textarea.on 'focus', -> + $(@).closest('.md-area').addClass 'is-focused' + + @textarea.on 'blur', -> + $(@).closest('.md-area').removeClass 'is-focused' diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 946d83b7bd..c7d74a12f9 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -10,6 +10,9 @@ class @Issue @initTaskList() @initIssueBtnEventListeners() + @initMergeRequests() + @initRelatedBranches() + initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -69,3 +72,23 @@ class @Issue type: 'PATCH' url: $('form.js-issuable-update').attr('action') data: patchData + + initMergeRequests: -> + $container = $('#merge-requests') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load referenced merge requests', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) + + initRelatedBranches: -> + $container = $('#related-branches') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load related branches', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d1fe116397..bc80980acb 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -34,7 +34,7 @@ class @LabelsSelect labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> issues?label_name=<%= label.title %>"> - + <%= label.title %> @@ -165,6 +165,8 @@ class @LabelsSelect .html(template) $sidebarCollapsedValue.text(labelCount) + $('.has-tooltip', $value).tooltip(container: 'body') + $value .find('a') .each((i) -> @@ -218,7 +220,7 @@ class @LabelsSelect selectable: true toggleLabel: (selected) -> - if selected and selected.title isnt 'Any Label' + if selected and selected.title? selected.title else defaultLabel diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee new file mode 100644 index 0000000000..ad1d1c7048 --- /dev/null +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -0,0 +1,17 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.formatDate = (datetime) -> + dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> + $timeagoEls.each( -> + $el = $(@) + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) + ) + + $timeagoEls.timeago() if setTimeago + +) window diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index 3f9ca39912..9e28353ac3 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -2,6 +2,11 @@ notificationGranted = (message, opts, onclick) -> notification = new Notification(message, opts) + # Hide the notification after X amount of seconds + setTimeout -> + notification.close() + , 8000 + if onclick notification.onclick = onclick diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 839e6ec2c0..1ab6e5114b 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -73,7 +73,8 @@ class @MergeRequestTabs @expandView() else if action == 'diffs' @loadDiff($target.attr('href')) - @shrinkView() + if bp? and bp.getBreakpointSize() isnt 'lg' + @shrinkView() else if action == 'builds' @loadBuilds($target.attr('href')) @expandView() @@ -84,8 +85,10 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - $el = $("div#{container} #{window.location.hash}") - $('body').scrollTo($el.offset().top) if $el.length + navBarHeight = $('.navbar-gitlab').outerHeight() + + $el = $("#{container} #{window.location.hash}") + $.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -141,7 +144,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#commits").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) @commitsLoaded = true @scrollToElement("#commits") @@ -151,12 +154,38 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.querySelector("div#diffs").innerHTML = data.html - $('.js-timeago').timeago() - $('div#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs').html data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) + $('#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") + @highlighSelectedLine() + + $(document) + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + + highlighSelectedLine: -> + $('.hll').removeClass 'hll' + locationHash = window.location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $(locationHash) + + if $diffLine.is ':not(tr)' + $diffLine = $("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $('td', $diffLine) + + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() loadBuilds: (source) -> return if @buildsLoaded @@ -165,7 +194,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#builds").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) @buildsLoaded = true @scrollToElement("#builds") diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 84a8887fbc..065626beeb 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -12,10 +12,19 @@ class @MergeRequestWidget @readyForCICheck = true clearInterval @fetchBuildStatusInterval + @clearEventListeners() + @addEventListeners() @pollCIStatus() notifyPermissions() - setOpts: (@opts) -> + clearEventListeners: -> + $(document).off 'page:change.merge_request' + + addEventListeners: -> + $(document).on 'page:change.merge_request', => + if $('body').data('page') isnt 'projects:merge_requests:show' + clearInterval @fetchBuildStatusInterval + @clearEventListeners() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -38,7 +47,7 @@ class @MergeRequestWidget $('.mr-state-widget').replaceWith(data) ciLabelForStatus: (status) -> - if status == 'success' + if status is 'success' 'passed' else status @@ -67,18 +76,28 @@ class @MergeRequestWidget @opts.ci_status = data.status return - if data.status isnt @opts.ci_status + if data.status isnt @opts.ci_status and data.status? @showCIStatus data.status if data.coverage @showCICoverage data.coverage if showNotification - message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + status = @ciLabelForStatus(data.status) + + if status is "preparing" + title = @opts.ci_title.preparing + status = status.charAt(0).toUpperCase() + status.slice(1); + message = @opts.ci_message.preparing.replace('{{status}}', status) + else + title = @opts.ci_title.normal + message = @opts.ci_message.normal.replace('{{status}}', status) + + title = title.replace('{{status}}', status) message = message.replace('{{sha}}', data.sha) message = message.replace('{{title}}', data.title) notify( - "Build #{@ciLabelForStatus(data.status)}", + title, message, @opts.gitlab_icon, -> @@ -98,6 +117,8 @@ class @MergeRequestWidget @setMergeButtonClass('btn-danger') when "running", "pending" @setMergeButtonClass('btn-warning') + when "success" + @setMergeButtonClass('btn-create') else $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') @@ -107,4 +128,6 @@ class @MergeRequestWidget $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request').removeClass("btn-create").addClass(css_class) + $('.accept_merge_request') + .removeClass('btn-danger btn-warning btn-create') + .addClass(css_class) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index f73127f49f..6bd4e885a0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -85,15 +85,21 @@ class @MilestoneSelect # display:block overrides the hide-collapse rule $value.removeAttr('style') clicked: (selected) -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is page is 'projects:merge_requests:index' + if $dropdown.hasClass 'js-filter-bulk-update' return - if $dropdown.hasClass('js-filter-submit') + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if selected.name? selectedMilestone = selected.name else selectedMilestone = '' Issues.filterResults $dropdown.closest('form') + else if $dropdown.hasClass('js-filter-submit') + $dropdown.closest('form').submit() else selected = $selectbox .find('input[type="hidden"]') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 86e3b860fc..fa91baa07c 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -163,9 +163,15 @@ class @Notes else if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list') + $notesList = $('ul.main-notes-list') + + $notesList .append(note.html) .syntaxHighlight() + + # Update datetime format on the recent note + gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false) + @initTaskList() @updateNotesCount(1) @@ -217,6 +223,8 @@ class @Notes # append new note to all matching discussions discussionContainer.append note_html + gl.utils.localTimeAgo($('.js-timeago', note_html), false) + @updateNotesCount(1) ### @@ -275,32 +283,10 @@ class @Notes show the form ### setupNoteForm: (form) -> - disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") - form.removeClass "js-new-note-form" - form.find('.div-dropzone').remove() - - # hide discard button - form.find('.js-note-discard').hide() - - # setup preview buttons - previewButton = form.find(".js-md-preview-button") + new GLForm form textarea = form.find(".js-note-text") - textarea.on "input", -> - if $(this).val().trim() isnt "" - previewButton.removeClass("turn-off").addClass "turn-on" - else - previewButton.removeClass("turn-on").addClass "turn-off" - - textarea.on 'focus', -> - $(this).closest('.md-area').addClass 'is-focused' - - textarea.on 'blur', -> - $(this).closest('.md-area').removeClass 'is-focused' - - autosize(textarea) - new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -309,11 +295,6 @@ class @Notes form.find("#note_noteable_id").val() ] - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - form.show() - ### Called in response to the new note form being submitted @@ -345,7 +326,9 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) - $('.js-timeago', $html).timeago() + + gl.utils.localTimeAgo($('.js-timeago', $html)) + $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') @@ -365,34 +348,15 @@ class @Notes note = $(this).closest(".note") note.addClass "is-editting" form = note.find(".note-edit-form") - isNewForm = form.is(':not(.gfm-form)') - if isNewForm - form.addClass('gfm-form') + form.addClass('current-note-edit-form') # Show the attachment delete link note.find(".js-note-attachment-delete").show() - # Setup markdown form - if isNewForm - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) + new GLForm form - textarea = form.find("textarea") - textarea.focus() - - if isNewForm - autosize(textarea) - - # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). - # The textarea has the correct value, Chrome just won't show it unless we - # modify it, so let's clear it and re-set it! - value = textarea.val() - textarea.val "" - textarea.val value - - if isNewForm - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + form.find(".js-note-text").focus() ### Called in response to clicking the edit note link @@ -549,6 +513,9 @@ class @Notes removeDiscussionNoteForm: (form)-> row = form.closest("tr") + glForm = form.data 'gl-form' + glForm.destroy() + form.find(".js-note-text").data("autosave").reset() # show the reply button (will only work for replies) @@ -560,7 +527,6 @@ class @Notes # only remove the form form.remove() - cancelDiscussionForm: (e) => e.preventDefault() form = $(e.target).closest(".js-discussion-note-form") diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index ae87c6c4e4..f4a2562885 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -18,8 +18,11 @@ class @Profile $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() - $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enable() + $('.update-notifications').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") @bindEvents() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 87d313ed67..07be85a32a 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -37,19 +37,20 @@ class @Project $('.update-notification').on 'click', (e) -> e.preventDefault() notification_level = $(@).data 'notification-level' - $('#notification_level').val(notification_level) + label = $(@).data 'notification-title' + $('#notification_setting_level').val(notification_level) $('#notification-form').submit() - label = null - switch notification_level - when 0 then label = ' Disabled ' - when 1 then label = ' Participating ' - when 2 then label = ' Watching ' - when 3 then label = ' Global ' - when 4 then label = ' On Mention ' $('#notifications-button').empty().append("" + label + "") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' + $('#notification-form').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") + + @projectSelectDropdown() projectSelectDropdown: -> diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index be8ab9b428..704bd8dee5 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,5 +1,37 @@ class @ProjectSelect constructor: -> + $('.js-projects-dropdown-toggle').each (i, dropdown) -> + $dropdown = $(dropdown) + + $dropdown.glDropdown( + filterable: true + filterRemote: true + search: + fields: ['name_with_namespace'] + data: (term, callback) -> + finalCallback = (projects) -> + callback projects + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups term, false, groupsCallback + else + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, term, projectsCallback + else + Api.projects term, @orderBy, projectsCallback + url: (project) -> + project.web_url + text: (project) -> + project.name_with_namespace + ) + $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index e177851124..860d4f438d 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,6 +4,7 @@ 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/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 084f0e0dc6..e4b7a3172e 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -10,10 +10,10 @@ class @Subscription btn = $(event.currentTarget) action = btn.find('span').text() current_status = @subscription_status.attr('data-status') - btn.prop('disabled', true) + btn.addClass('disabled') $.post @url, => - btn.prop('disabled', false) + btn.removeClass('disabled') status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed' @subscription_status.attr('data-status', status) action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index ec2df6c5b7..00d2b64172 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -57,5 +57,12 @@ class @Todos $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count - goToTodoUrl: -> - Turbolinks.visit($(this).data('url')) + goToTodoUrl: (e)-> + todoLink = $(this).data('url') + return unless todoLink + + if e.metaKey + e.preventDefault() + window.open(todoLink,'_blank') + else + Turbolinks.visit(todoLink) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 657c5f033c..e8c0172680 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -7,6 +7,7 @@ &:focus, &:active { outline: none; + background-color: $btn-active-gray; @include box-shadow($gl-btn-active-background); } } @@ -27,7 +28,8 @@ color: $color; } - &:active { + &:active, + &.active { @include box-shadow ($gl-btn-active-background); background-color: $dark; @@ -61,7 +63,7 @@ } @mixin btn-white { - @include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236); + @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); } .btn { @@ -218,3 +220,26 @@ margin-right: 5px; } } + +.btn-text-field { + width: 100%; + text-align: left; + padding: 6px 16px; + border-color: $border-color; + color: $btn-placeholder-gray; + background-color: $background-color; + + &:hover, + &:active, + &:focus { + cursor: text; + box-shadow: none; + border-color: $border-color; + color: $btn-placeholder-gray; + background-color: $background-color; + } +} + +.btn-file-option { + background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 82dc1acbd0..ba6c7930cd 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -248,7 +248,7 @@ .dropdown-title { position: relative; - padding: 0 0 15px; + padding: 0 25px 15px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -275,7 +275,7 @@ } .dropdown-menu-close { - right: 7px; + right: 5px; width: 20px; height: 20px; top: -1px; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index b15f4e7bd5..789df42fb6 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -15,12 +15,13 @@ .file-title { position: relative; - background: $background-color; + background-color: $background-color; border-bottom: 1px solid $border-color; margin: 0; text-align: left; padding: 10px $gl-padding; word-wrap: break-word; + border-radius: 3px 3px 0 0; .file-actions { float: right; @@ -49,7 +50,7 @@ } } - a { + a:not(.btn) { color: $gl-dark-link-color; } diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 5ae0520fd7..f4d35c4b4b 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -1,24 +1,6 @@ /** * Styles that apply to all GFM related forms. */ -.issue-form, .merge-request-form, .wiki-form { - .description { - height: 16em; - border-top-left-radius: 0; - } -} - -.wiki-form { - .description { - height: 26em; - } -} - -.milestone-form { - .description { - height: 14em; - } -} .gfm-commit, .gfm-commit_range { font-family: $monospace_font; diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index fa9038ebac..c83cf88159 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -33,15 +33,10 @@ 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; @@ -67,6 +62,7 @@ .count { color: $color-light; + background: $color-dark; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 724980b220..3f015427d0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -69,6 +69,7 @@ header { } .header-content { + position: relative; height: $header-height; padding-right: 20px; @@ -76,6 +77,10 @@ header { padding-right: 0; } + .dropdown-menu { + margin-top: -5px; + } + .title { margin: 0; font-size: 19px; @@ -123,11 +128,11 @@ header { } @mixin collapsed-header { - margin-left: 40px; + margin-left: $sidebar_collapsed_width; } .header-collapsed { - margin-left: 40px; + margin-left: $sidebar_collapsed_width; @media (min-width: $screen-md-min) { @include collapsed-header; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index c8f86d60e3..0f32d36d59 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,22 +1,15 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - margin-bottom: -5px; - - .div-dropzone-focus { - border-color: #66afe9 !important; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important; - outline: 0 !important; - } .div-dropzone-hover { position: absolute; top: 50%; left: 50%; - margin-top: -0.5em; - margin-left: -0.6em; + margin-top: -11.5px; + margin-left: -15px; opacity: 0; - font-size: 50px; + font-size: 30px; transition: opacity 200ms ease-in-out; pointer-events: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 94f5a12ff6..192d53b048 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,12 +58,12 @@ .nav-search { display: inline-block; - width: 50%; + width: 100%; padding: 11px 0; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { - width: 100%; + @media (min-width: $screen-sm-min) { + width: 50%; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 1d49249dd8..18189e985c 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,7 +144,7 @@ } a { - padding: 7px 12px; + padding: 7px 15px; font-size: $gl-font-size; line-height: 24px; color: $gray; @@ -169,12 +169,10 @@ } .count { - &:before { - content: '('; - } - &:after { - content: ')'; - } + float: right; + background: #eee; + padding: 0 8px; + @include border-radius(6px); } &.back-link i { @@ -193,27 +191,6 @@ } } -.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; @@ -233,12 +210,55 @@ } .page-sidebar-collapsed { + padding-left: $sidebar_collapsed_width; + .sidebar-wrapper { - display: none; + 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; + } + } } } .page-sidebar-expanded { + padding-left: $sidebar_collapsed_width; + @media (min-width: $screen-md-min) { padding-left: $sidebar_width; } @@ -289,48 +309,3 @@ 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/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index aa244fe548..b91f2f6f89 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -14,10 +14,6 @@ background: $row-hover; } - &:last-child { - border-bottom: none; - } - .avatar { margin-right: 15px; } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index dd42db1840..96bab7880c 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -43,7 +43,6 @@ @import "bootstrap/modals"; @import "bootstrap/tooltip"; @import "bootstrap/popovers"; -@import "bootstrap/carousel"; // Utility classes .clearfix { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 7b2aada5a0..0a5b4b8834 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -250,14 +250,6 @@ a > code { * Textareas intended for GFM * */ -.js-gfm-input { - font-family: $monospace_font; - color: $gl-text-color; -} - -.md-preview { -} - .strikethrough { text-decoration: line-through; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8d3ad934a5..f910cf6181 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -10,10 +10,10 @@ $gutter_inner_width: 258px; /* * UI elements */ -$border-color: #efeff1; +$border-color: #e5e5e5; $focus-border-color: #3aabf0; $table-border-color: #eef0f2; -$background-color: #faf9f9; +$background-color: #fafafa; /* * Text @@ -28,6 +28,7 @@ $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; +$gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -81,7 +82,7 @@ $provider-btn-not-active-color: #4688f1; $white-light: #fff; $white-normal: #ededed; -$white-dark: #ededed; +$white-dark: #ececec; $gray-light: #faf9f9; $gray-normal: #f5f5f5; @@ -108,6 +109,8 @@ $red-light: #e52c5a; $red-normal: #d22852; $red-dark: darken($red-normal, 5%); +$black-transparent: rgba(0, 0, 0, 0.3); + $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; $border-white-dark: #c6cacf; @@ -147,18 +150,26 @@ $light-grey-header: #faf9f9; */ $gl-primary: $blue-normal; $gl-success: $green-normal; +$gl-success-focus: rgba($gl-success, .4); $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; -$gl-btn-active-background: rgba(0, 0, 0, 0.12); -$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background; +$gl-btn-active-background: rgba(0, 0, 0, 0.16); +$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* * Commit Diff Colors */ $added: #63c363; $deleted: #f77; - +$line-added: #ecfdf0; +$line-added-dark: #c7f0d2; +$line-removed: #fbe9eb; +$line-removed-dark: #fac5cd; +$line-number-old: #f9d7dc; +$line-number-new: #ddfbe6; +$match-line: #fafafa; +$table-border-gray: #f0f0f0; /* * Fonts */ @@ -191,6 +202,13 @@ $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); $dropdown-toggle-icon-color: #c4c4c4; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; +/* +* Buttons +*/ +$btn-active-gray: #ececec; +$btn-placeholder-gray: #c7c7c7; +$btn-white-active: #848484; + /* * Award emoji */ diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 4767394489..77a73dc379 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #557; + border-color: darken(#557, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 806401c21a..28253d4ccb 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #49483e; + border-color: darken(#49483e, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 6a809d4dfd..c62bd021ae 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #174652; + border-color: darken(#174652, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index b90c95c62d..524cfaf90c 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -6,7 +6,7 @@ } .diff-line-num, .diff-line-num a { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; } // Code itself @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #ddd8c5; + border-color: darken(#ddd8c5, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } @@ -30,7 +36,7 @@ } .line_content.match { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; background: rgba(255, 255, 255, 0.4); } } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 8c1b0cd84e..1ff6ad75e0 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -6,12 +6,12 @@ } .diff-line-num, .diff-line-num a { - color: rgba(0, 0, 0, 0.3); + color: $black-transparent; } // Code itself pre.code, .diff-line-num { - border-color: $border-color; + border-color: $table-border-gray; } &, pre.code, .line_holder .line_content { @@ -21,38 +21,44 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #f8eec7; + border-color: darken(#f8eec7, 15%); + } + .diff-line-num { &.old { - background: #fdd; - border-color: #f1c0c0; + background-color: $line-number-old; + border-color: $line-removed-dark; } &.new { - background: #dbffdb; - border-color: #c1e9c1; + background-color: $line-number-new; + border-color: $line-added-dark; } } .line_content { &.old { - background: #ffecec; + background: $line-removed; span.idiff { - background-color: #f8cbcb; + background-color: $line-removed-dark; } } &.new { - background: #eaffea; + background-color: $line-added; span.idiff { - background-color: #a6f3a6; + background-color: $line-added-dark; } } &.match { - color: rgba(0, 0, 0, 0.3); - background: #fafafa; + color: $black-transparent; + background: $match-line; } } } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 082911bd11..358d2f4ab9 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -20,6 +20,8 @@ margin: 0; padding: 0; margin-top: 10px; + word-break: normal; + white-space: pre-wrap; } .commit-info-row { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 8272615768..6453c91d95 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -47,6 +47,7 @@ li.commit { .commit_short_id { min-width: 65px; + color: $gl-dark-link-color; font-family: $monospace_font; } @@ -88,6 +89,10 @@ li.commit { padding: 0; margin: 0; } + + a { + color: $gl-dark-link-color; + } } .commit-row-info { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7a12aa9647..d0855f6691 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -2,6 +2,7 @@ .diff-file { border: 1px solid $border-color; margin-bottom: $gl-padding; + border-radius: 3px; .diff-header { position: relative; @@ -10,6 +11,7 @@ padding: 10px 16px; color: #555; z-index: 10; + border-radius: 3px 3px 0 0; .diff-title { font-family: $monospace_font; @@ -31,6 +33,7 @@ overflow-y: hidden; background: #fff; color: #333; + border-radius: 0 0 3px 3px; .unfold { cursor: pointer; @@ -63,10 +66,10 @@ .line_holder td { line-height: $code_line_height; font-size: $code_font_size; - } - td { - white-space: nowrap; + span { + white-space: pre; + } } } @@ -109,6 +112,10 @@ display: table-cell; } } + + .text-file.diff-wrap-lines table .line_holder td span { + white-space: pre-wrap; + } } .image { background: #ddd; @@ -321,6 +328,16 @@ float: right; } +.diffs { + .content-block { + border-bottom: none; + } +} + +.files-changed { + border-bottom: none; +} + // Mobile @media (max-width: 480px) { .diff-title { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index c66efe978c..6fe57c737b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -41,8 +41,17 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; + + .label { + color: $gl-text-color; + font-size: inherit; + } + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index bd224705f0..604f1700cf 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,6 +59,9 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { + margin: -$gl-padding+1; + } } body.modal-open { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c7..d9218e1509 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -263,6 +263,12 @@ } } + .dropdown-content { + a:hover { + color: inherit; + } + } + .dropdown-menu-toggle { width: 100%; padding-top: 6px; @@ -316,3 +322,9 @@ color: #8c8c8c; } } + +.issuable-form-padding-top { + @media (min-width: $screen-sm-min) { + padding-top: 7px; + } +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 4e02ec4e89..da20fa2880 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -49,6 +49,15 @@ } .label-row { + .label-name { + display: inline-block; + width: 200px; + + @media (max-width: $screen-xs-min) { + display: block; + } + } + .label { padding: 9px; font-size: 14px; @@ -69,3 +78,63 @@ background-color: $gl-danger; color: $white-light; } + +@mixin labels-mobile { + @media (max-width: $screen-xs-min) { + display: block; + width: 100%; + margin-left: 0; + padding: 10px 0; + } +} + + +.manage-labels-list { + + .prepend-left-10, .prepend-description-left { + display: inline-block; + width: 40%; + vertical-align: middle; + + @include labels-mobile; + } + + .prepend-description-left { + width: 57%; + + @include labels-mobile; + } + + .pull-info-right { + float: right; + + @media (max-width: $screen-xs-min) { + float: none; + } + + .action-buttons { + border-color: transparent; + padding: 6px; + color: $gl-text-color; + + &.subscribe-button { + padding-left: 0; + } + } + + i { + color: $gl-text-color; + } + + .append-right-20 { + a { + color: $gl-text-color; + } + + @media (max-width: $screen-xs-min) { + display: block; + margin-bottom: 10px; + } + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1c6a420897..4ef548ffbe 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -123,6 +123,8 @@ .mr_source_commit, .mr_target_commit { + margin-bottom: 0; + .commit { margin: 0; padding: 2px 0; @@ -140,6 +142,7 @@ overflow: hidden; font-size: 90%; margin: 0 3px; + word-break: break-all; } .mr-list { @@ -174,10 +177,6 @@ display: none; } -.merge-request-form .select2-container { - width: 250px !important; -} - #modal_merge_info .modal-dialog { width: 600px; @@ -200,3 +199,76 @@ overflow-x: scroll; } } + +.panel-new-merge-request { + .panel-heading { + padding: 5px 10px; + font-weight: 600; + line-height: 25px; + } + + .panel-body { + padding: 10px 5px; + } + + .panel-footer { + padding: 5px 10px; + } + + .commit { + .commit-row-title { + margin-bottom: 4px; + } + + .avatar { + width: 20px; + height: 20px; + margin-right: 5px; + } + + .commit-row-info { + line-height: 20px; + } + } + + .btn-clipboard { + margin-right: 5px; + padding: 0; + background: transparent; + } + + .ci-status-link { + margin-right: 5px; + } +} + +.merge-request-select { + padding-left: 5px; + padding-right: 5px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + @media (min-width: $screen-sm-min) { + float: left; + width: 50%; + margin-bottom: 0; + } + + .dropdown-menu-toggle { + width: 100%; + } + + .dropdown-menu { + left: 5px; + right: 5px; + width: auto; + } +} + +.issuable-form-select-holder { + display: inline-block; + width: 250px; +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index a909776b43..07c707e7b7 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -1,10 +1,10 @@ /** * Note Form */ -.reply-btn { - @extend .btn-primary; - margin: 10px $gl-padding; +.comment-btn { + @extend .btn-create; } + .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { opacity: 1.0; @@ -40,6 +40,7 @@ } .note-textarea { + display: block; padding: 10px 0; font-family: $regular_font; border: 0; @@ -63,7 +64,7 @@ &.is-focused { border-color: $focus-border-color; - box-shadow: 0 0 2px rgba(#000, .2), + box-shadow: 0 0 2px $black-transparent, 0 0 4px rgba($focus-border-color, .4); .comment-toolbar, @@ -71,12 +72,35 @@ border-color: $focus-border-color; } } + + &.is-dropzone-hover { + border-color: $gl-success; + box-shadow: 0 0 2px $black-transparent, + 0 0 4px $gl-success-focus; + + .comment-toolbar, + .nav-links { + border-color: $gl-success; + } + } + + p { + code { + white-space: normal; + } + + pre { + code { + white-space: pre; + } + } + } } } .discussion-form { padding: $gl-padding-top $gl-padding; - background-color: #fff; + background-color: $white-light; } .note-edit-form { @@ -113,13 +137,12 @@ .discussion-body, .diff-file { .notes .note { - border-color: #ddd; padding: 10px 15px; } .discussion-reply-holder { - background: $background-color; - border-top: 1px solid $border-color; + background-color: $white-light; + padding: 10px 16px; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index aca86457c7..e421a31549 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -58,6 +58,7 @@ ul.notes { .note { display: block; position: relative; + border-bottom: 1px solid $table-border-gray; &.is-editting { .note-header, @@ -80,9 +81,15 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - pre { + p { code { - white-space: pre; + white-space: normal; + } + + pre { + code { + white-space: pre; + } } } @@ -111,15 +118,16 @@ ul.notes { margin: 10px 0; } } + + a { + word-break: break-all; + } } .note-header { padding-bottom: 3px; } - &:last-child { - border-bottom: 1px solid $border-color; - } } } @@ -129,7 +137,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre-wrap; + white-space: pre; } } @@ -137,29 +145,37 @@ ul.notes { font-family: $regular_font; td { - border: 1px solid #ddd; + border: 1px solid $table-border-gray; border-left: none; &.notes_line { vertical-align: middle; text-align: center; padding: 10px 0; - background: #fff; + background: $background-color; color: $text-color; } + &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } + &.notes_content { - background-color: #fff; + background-color: $background-color; border-width: 1px 0; padding: 0; vertical-align: top; + white-space: normal; + &.parallel { border-width: 1px; } + + .notes { + background-color: $white-light; + } } } } @@ -175,9 +191,6 @@ ul.notes { } } - .author_link { - font-weight: 600; - } } .note-headline-light, @@ -203,14 +216,26 @@ ul.notes { line-height: 24px; .fa { + color: $notes-action-color; position: relative; top: 1px; font-size: 17px; } - .fa-trash-o { - top: 0; - font-size: 16px; + &.js-note-delete { + i { + &:hover { + color: $gl-text-red; + } + } + } + + &.js-note-edit { + i { + &:hover { + color: $gl-link-color; + } + } } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e83fa9e3d5..75f78569e3 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -34,6 +34,11 @@ color: #7f8fa4; font-size: $gl-font-size; + .label { + color: $gl-text-color; + font-size: inherit; + } + p { color: #5c5d5e; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c81cb85dc1..ce5c84ee9b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,7 @@ require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings + include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper @@ -47,6 +48,16 @@ class ApplicationController < ActionController::Base email: current_user.email, username: current_user.username, ) + + Raven.tags_context(program: sentry_program_context) + end + end + + def sentry_program_context + if Sidekiq.server? + 'sidekiq' + else + 'rails' end end @@ -148,20 +159,6 @@ class ApplicationController < ActionController::Base end end - def add_gon_variables - gon.api_version = API::API.version - gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.default_issues_tracker = Project.new.default_issue_tracker.to_param - gon.max_file_size = current_application_settings.max_attachment_size - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - - if current_user - gon.current_user_id = current_user.id - gon.api_token = current_user.private_token - end - end - def validate_user_service_ticket! return unless signed_in? && session[:service_tickets] diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index b23c3022fb..9d5a28e8d4 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -18,14 +18,14 @@ class Groups::MilestonesController < Groups::ApplicationController end def create - project_ids = params[:milestone][:project_ids] + project_ids = params[:milestone][:project_ids].reject(&:blank?) title = milestone_params[:title] - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute + if create_milestones(project_ids) + redirect_to milestone_path(title) + else + render_new_with_error(project_ids.empty?) end - - redirect_to milestone_path(title) end def show @@ -41,6 +41,27 @@ class Groups::MilestonesController < Groups::ApplicationController private + def create_milestones(project_ids) + return false unless project_ids.present? + + ActiveRecord::Base.transaction do + @projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute + end + end + + true + rescue ActiveRecord::ActiveRecordError => e + flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}" + false + end + + def render_new_with_error(empty_project_ids) + @milestone = Milestone.new(milestone_params) + @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids + render :new + end + def authorize_admin_milestones! return render_404 unless can?(current_user, :admin_milestones, group) end diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb new file mode 100644 index 0000000000..de13b16ccf --- /dev/null +++ b/app/controllers/groups/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Groups::NotificationSettingsController < Groups::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(group) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index d1e4ac10f6..c6bdd0602c 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,9 +1,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings + include Gitlab::GonHelper include PageLayoutHelper before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! + before_action :add_gon_variables layout 'profile' diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index d28e96c3f1..df98f56a1c 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -60,6 +60,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end + rescue Gitlab::OAuth::SignupDisabledError + handle_signup_error end def omniauth_error @@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end rescue Gitlab::OAuth::SignupDisabledError - label = Gitlab::OAuth::Provider.label_for(oauth['provider']) - message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." - - if current_application_settings.signup_enabled? - message << " Create a GitLab account first, and then connect it to your #{label} account." - end - - flash[:notice] = message - - redirect_to new_user_session_path + handle_signup_error end def handle_service_ticket provider, ticket @@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end + def handle_signup_error + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{label} account." + end + + flash[:notice] = message + + redirect_to new_user_session_path + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 1fd1d6882d..18ee55c839 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,39 +1,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user - @notification = current_user.notification - @project_members = current_user.project_members - @group_members = current_user.group_members + @group_notifications = current_user.notification_settings.for_groups + @project_notifications = current_user.notification_settings.for_projects end def update - type = params[:notification_type] - - @saved = if type == 'global' - current_user.update_attributes(user_params) - elsif type == 'group' - group_member = current_user.group_members.find(params[:notification_id]) - group_member.notification_level = params[:notification_level] - group_member.save - else - project_member = current_user.project_members.find(params[:notification_id]) - project_member.notification_level = params[:notification_level] - project_member.save - end - - respond_to do |format| - format.html do - if @saved - flash[:notice] = "Notification settings saved" - else - flash[:alert] = "Failed to save new settings" - end - - redirect_back_or_default(default: profile_notifications_path) - end - - format.js + if current_user.update_attributes(user_params) + flash[:notice] = "Notification settings saved" + else + flash[:alert] = "Failed to save new settings" end + + redirect_back_or_default(default: profile_notifications_path) end def user_params diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6d649e72f8..c26cfeccf1 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions before_action :module_enabled - before_action :issue, only: [:edit, :update, :show] + before_action :issue, + only: [:edit, :update, :show, :referenced_merge_requests, :related_branches] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow issues bulk update before_action :authorize_admin_issues!, only: [:bulk_update] - # Cross-reference merge requests - before_action :closed_by_merge_requests, only: [:show] - respond_to :html def index @@ -65,8 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue - @merge_requests = @issue.referenced_merge_requests(current_user) - @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) respond_to do |format| format.html @@ -118,15 +114,39 @@ class Projects::IssuesController < Projects::ApplicationController end end + def referenced_merge_requests + @merge_requests = @issue.referenced_merge_requests(current_user) + @closed_by_merge_requests = @issue.closed_by_merge_requests(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_merge_requests') + } + end + end + end + + def related_branches + merge_requests = @issue.referenced_merge_requests(current_user) + + @related_branches = @issue.related_branches - + merge_requests.map(&:source_branch) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_related_branches') + } + end + end + end + def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end - def closed_by_merge_requests - @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) - end - protected def issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 49064f5d50..3e0cfc6aa6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -207,20 +207,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController #This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @commit = @repository.commit(params[:ref]) if params[:ref].present? + render layout: false end def branch_to @target_project = selected_target_project @commit = @target_project.commit(params[:ref]) if params[:ref].present? + render layout: false end def update_branches @target_project = selected_target_project @target_branches = @target_project.repository.branch_names - respond_to do |format| - format.js - end + render layout: false end def ci_status @@ -237,6 +237,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + status = "preparing" if status.nil? + response = { title: merge_request.title, sha: merge_request.last_commit_short_sha, diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd56804..707a0d0e5c 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController def destroy if note.editable? - note.destroy - note.reset_events_cache + Notes::DeleteService.new(project, current_user).execute(note) end respond_to do |format| diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb new file mode 100644 index 0000000000..7d81cc03c7 --- /dev/null +++ b/app/controllers/projects/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Projects::NotificationSettingsController < Projects::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(project) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 5c7614cfba..bb7a6b6a5a 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - RepositoryArchiveCacheWorker.perform_async headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) head :ok rescue => ex diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3cc37e5985..3768efe142 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do + if current_user + @membership = @project.team.find_member(current_user.id) + + if @membership + @notification_setting = current_user.notification_settings_for(@project) + end + end + if @project.repository_exists? if @project.empty_repo? render 'projects/empty' else - if current_user - @membership = @project.team.find_member(current_user.id) - end - render :show end else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e6ceb21353..16e5b8ac22 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -184,7 +184,7 @@ module ApplicationHelper element = content_tag :time, time.to_s, class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", datetime: time.to_time.getutc.iso8601, - title: time.in_time_zone.to_s(:medium), + title: time.to_time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 820d69c230..9e59a295fc 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -27,9 +27,9 @@ module BlobHelper link_opts) if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } + button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) - link_to "Edit", edit_path, class: 'btn' + link_to "Edit", edit_path, class: 'btn btn-file-option' elsif can?(current_user, :fork_project, project) continue_params = { to: edit_path, @@ -38,7 +38,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn', method: :post + link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index bde0799f3d..35ba543cef 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -28,7 +28,7 @@ module CommitsHelper def commit_to_html(commit, project, inline = true) template = inline ? "inline_commit" : "commit" - escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? + render "projects/commits/#{template}", commit: commit, project: project unless commit.nil? end # Breadcrumb links for a Project and, if applicable, a tree path @@ -117,7 +117,7 @@ module CommitsHelper end end link_to( - "Browse Files »", + "Browse Files", namespace_project_tree_path(project.namespace, project, commit), class: "pull-right" ) @@ -197,7 +197,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff.new_path)), - class: 'btn view-file js-view-file' + class: 'btn view-file js-view-file btn-file-option' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb new file mode 100644 index 0000000000..6a43be2cf3 --- /dev/null +++ b/app/helpers/form_helper.rb @@ -0,0 +1,18 @@ +module FormHelper + def form_errors(model) + return unless model.errors.any? + + pluralized = 'error'.pluralize(model.errors.count) + headline = "The form contains the following #{pluralized}:" + + content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do + content_tag(:h4, headline) << + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end + end + end +end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 2f760af02f..3a45205563 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -116,29 +116,6 @@ module GitlabMarkdownHelper end end - MARKDOWN_TIPS = [ - "End a line with two or more spaces for a line-break, or soft-return", - "Inline code can be denoted by `surrounding it with backticks`", - "Blocks of code can be denoted by three backticks ``` or four leading spaces", - "Emoji can be added by :emoji_name:, for example :thumbsup:", - "Notify other participants using @user_name", - "Notify a specific group using @group_name", - "Notify the entire team using @all", - "Reference an issue using a hash, for example issue #123", - "Reference a merge request using an exclamation point, for example MR !123", - "Italicize words or phrases using *asterisks* or _underscores_", - "Bold words or phrases using **double asterisks** or __double underscores__", - "Strikethrough words or phrases using ~~two tildes~~", - "Make a bulleted list using + pluses, - minuses, or * asterisks", - "Denote blockquotes using > at the beginning of a line", - "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___" - ].freeze - - # Returns a random markdown tip for use as a textarea placeholder - def random_markdown_tip - MARKDOWN_TIPS.sample - end - private # Return +text+, truncated to +max_chars+ characters, excluding any HTML diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 24b90fef4f..4cb8adceba 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -52,6 +52,7 @@ module IssuesHelper def milestone_options(object) milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a + milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) @@ -115,17 +116,32 @@ module IssuesHelper icon('eye-slash') if issue.confidential? end - def emoji_icon(name, unicode = nil, aliases = []) + def emoji_icon(name, unicode = nil, aliases = [], sprite: true) unicode ||= Emoji.emoji_filename(name) rescue "" - content_tag :div, "", - class: "icon emoji-icon emoji-#{unicode}", - title: name, - data: { - aliases: aliases.join(' '), - emoji: name, - unicode_name: unicode - } + data = { + aliases: aliases.join(" "), + emoji: name, + unicode_name: unicode + } + + if sprite + # Emoji icons for the emoji menu, these use a spritesheet. + content_tag :div, "", + class: "icon emoji-icon emoji-#{unicode}", + title: name, + data: data + else + # Emoji icons displayed separately, used for the awards already given + # to an issue or merge request. + content_tag :img, "", + class: "icon emoji", + title: name, + height: "20px", + width: "20px", + src: url_to_image("#{unicode}.png"), + data: data + end end def emoji_author_list(notes, current_user) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index faba418c4d..94c6b548ec 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -3,8 +3,16 @@ module NamespacesHelper groups = current_user.owned_groups + current_user.masters_groups users = [current_user.namespace] - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] + data_attr_group = { 'data-options-parent' => 'groups' } + data_attr_users = { 'data-options-parent' => 'users' } + + group_opts = [ + "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] } + ] + + users_opts = [ + "Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] } + ] options = [] options << group_opts diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 698f90cb27..95072b5373 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -69,10 +69,7 @@ module NotesHelper line_type: line_type } - button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button', - data: data, title: 'Add a reply' do - link_text = icon('comment') - link_text << ' Reply' - end + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', + data: data, title: 'Add a reply' end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 499c655d2b..54ab9179ef 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,48 +1,48 @@ module NotificationsHelper include IconsHelper - def notification_icon(notification) - if notification.disabled? - icon('volume-off', class: 'ns-mute') - elsif notification.participating? - icon('volume-down', class: 'ns-part') - elsif notification.watch? - icon('volume-up', class: 'ns-watch') - else - icon('circle-o', class: 'ns-default') + def notification_icon_class(level) + case level.to_sym + when :disabled + 'microphone-slash' + when :participating + 'volume-up' + when :watch + 'eye' + when :mention + 'at' + when :global + 'globe' end end - def notification_list_item(notification_level, user_membership) - case notification_level - when Notification::N_DISABLED - update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') - when Notification::N_PARTICIPATING - update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') - when Notification::N_WATCH - update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') - when Notification::N_MENTION - update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') - when Notification::N_GLOBAL - update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') + def notification_icon(level, text = nil) + icon("#{notification_icon_class(level)} fw", text: text) + end + + def notification_title(level) + case level.to_sym + when :participating + 'Participate' + when :mention + 'On mention' else - # do nothing + level.to_s.titlecase end end - def update_notification_link(notification_level, user_membership, title, icon) - content_tag(:li, class: active_level_for(user_membership, notification_level)) do - link_to '#', class: 'update-notification', data: { notification_level: notification_level } do - icon("#{icon} fw", text: title) + def notification_list_item(level, setting) + title = notification_title(level) + + data = { + notification_level: level, + notification_title: title + } + + content_tag(:li, class: ('active' if setting.level == level)) do + link_to '#', class: 'update-notification', data: data do + notification_icon(level, title) end end end - - def notification_label(user_membership) - Notification.new(user_membership).to_s - end - - def active_level_for(user_membership, level) - 'active' if user_membership.notification_level == level - end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4e4c6e301d..7e00aaccea 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -65,21 +65,14 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to project_path(project), { class: "project-item-select-holder" } do - link_output = simple_sanitize(project.name) + project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } - if current_user - link_output += project_select_tag :project_path, - class: "project-item-select js-projects-dropdown", - data: { include_groups: false, order_by: 'last_activity_at' } - end - - link_output + if current_user + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) end - project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user - full_title = namespace_link + ' / ' + project_link - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title = "#{namespace_link} / #{project_link}".html_safe + full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name full_title end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index edc5686cf0..2f06668218 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -20,6 +20,8 @@ module TodosHelper end def todo_target_path(todo) + return unless todo.target.present? + anchor = dom_id(todo.note) if todo.note.present? if todo.for_commit? diff --git a/app/models/commit.rb b/app/models/commit.rb index d09876a07d..d1f07ccd55 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -150,13 +150,11 @@ class Commit end def hook_attrs(with_changed_files: false) - path_with_namespace = project.path_with_namespace - data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", + url: Gitlab::UrlBuilder.build(self), author: { name: author_name, email: author_email diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb deleted file mode 100644 index d7dcd97911..0000000000 --- a/app/models/concerns/notifiable.rb +++ /dev/null @@ -1,15 +0,0 @@ -# == Notifiable concern -# -# Contains notification functionality -# -module Notifiable - extend ActiveSupport::Concern - - included do - validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true - end - - def notification - @notification ||= Notification.new(self) - end -end diff --git a/app/models/group.rb b/app/models/group.rb index b332601c59..9a04ac70d3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -27,6 +27,7 @@ class Group < Namespace has_many :users, through: :group_members has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project + has_many :notification_settings, dependent: :destroy, as: :source validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/issue.rb b/app/models/issue.rb index e064b0f8b9..3f188e0477 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -106,7 +106,7 @@ class Issue < ActiveRecord::Base def related_branches project.repository.branch_names.select do |branch| - branch.end_with?("-#{iid}") + branch =~ /\A#{iid}-(?!\d+-stable)/i end end @@ -151,7 +151,7 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{title.parameterize}-#{iid}" + "#{iid}-#{title.parameterize}" end def can_be_worked_on?(current_user) diff --git a/app/models/member.rb b/app/models/member.rb index ca08007b7e..60efafef21 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -19,7 +19,6 @@ class Member < ActiveRecord::Base include Sortable - include Notifiable include Gitlab::Access attr_accessor :raw_invite_token @@ -56,12 +55,15 @@ class Member < ActiveRecord::Base before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite? + after_create :create_notification_setting, unless: :invite? after_create :post_create_hook, unless: :invite? after_update :post_update_hook, unless: :invite? after_destroy :post_destroy_hook, unless: :invite? delegate :name, :username, :email, to: :user, prefix: true + default_value_for :notification_level, NotificationSetting.levels[:global] + class << self def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) @@ -160,6 +162,14 @@ class Member < ActiveRecord::Base send_invite end + def create_notification_setting + user.notification_settings.find_or_create_for(source) + end + + def notification_setting + @notification_setting ||= user.notification_settings_for(source) + end + private def send_invite diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 65d2ea0057..9fb474a1a9 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -24,7 +24,6 @@ class GroupMember < Member # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\ANamespace\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 560d1690e1..07ddb02ae9 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -27,7 +27,6 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bf185cb5dd..e410febdff 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches + validate :validate_branches, unless: :allow_broken validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } @@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base end if opened? || reopened? - similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened + similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, @@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base def hook_attrs attrs = { - source: source_project.hook_attrs, + source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, work_in_progress: work_in_progress? diff --git a/app/models/notification.rb b/app/models/notification.rb deleted file mode 100644 index 171b8df45c..0000000000 --- a/app/models/notification.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Notification - # - # Notification levels - # - N_DISABLED = 0 - N_PARTICIPATING = 1 - N_WATCH = 2 - N_GLOBAL = 3 - N_MENTION = 4 - - attr_accessor :target - - class << self - def notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH] - end - - def options_with_labels - { - disabled: N_DISABLED, - participating: N_PARTICIPATING, - watch: N_WATCH, - mention: N_MENTION, - global: N_GLOBAL - } - end - - def project_notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL] - end - end - - def initialize(target) - @target = target - end - - def disabled? - target.notification_level == N_DISABLED - end - - def participating? - target.notification_level == N_PARTICIPATING - end - - def watch? - target.notification_level == N_WATCH - end - - def global? - target.notification_level == N_GLOBAL - end - - def mention? - target.notification_level == N_MENTION - end - - def level - target.notification_level - end - - def to_s - case level - when N_DISABLED - 'Disabled' - when N_PARTICIPATING - 'Participating' - when N_WATCH - 'Watching' - when N_MENTION - 'On mention' - when N_GLOBAL - 'Global' - else - # do nothing - end - end -end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb new file mode 100644 index 0000000000..5001738f41 --- /dev/null +++ b/app/models/notification_setting.rb @@ -0,0 +1,28 @@ +class NotificationSetting < ActiveRecord::Base + enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } + + default_value_for :level, NotificationSetting.levels[:global] + + belongs_to :user + belongs_to :source, polymorphic: true + + validates :user, presence: true + validates :source, presence: true + validates :level, presence: true + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } + + scope :for_groups, -> { where(source_type: 'Namespace') } + scope :for_projects, -> { where(source_type: 'Project') } + + def self.find_or_create_for(source) + setting = find_or_initialize_by(source: source) + + unless setting.persisted? + setting.save + end + + setting + end +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb new file mode 100644 index 0000000000..c78c7f4aa0 --- /dev/null +++ b/app/models/oauth_access_token.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +class OauthAccessToken < ActiveRecord::Base + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/project.rb b/app/models/project.rb index 6304699386..b82825906f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -154,6 +154,7 @@ class Project < ActiveRecord::Base has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -388,9 +389,15 @@ class Project < ActiveRecord::Base def add_import_job if forked? - RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(self.id) + job_id = RepositoryImportWorker.perform_async(self.id) + end + + if job_id + Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{path_with_namespace}" end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 9e7f642180..060062aaf7 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -82,17 +82,17 @@ class BambooService < CiService end def build_info(sha) - url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") + url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s if username.blank? && password.blank? - @response = HTTParty.get(parsed_url.to_s, verify: false) + @response = HTTParty.get(url, verify: false) else - get_url = "#{url}&os_authType=basic" + url << '&os_authType=basic' auth = { - username: username, - password: password, + username: username, + password: password } - @response = HTTParty.get(get_url, verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end end @@ -101,11 +101,11 @@ class BambooService < CiService if @response.code != 200 || @response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. - "#{bamboo_url}/browse/#{build_key}" + URI.join(bamboo_url, "/browse/#{build_key}").to_s else # If actual build link is available, go to build result page. result_key = @response['results']['results']['result']['planResultKey']['key'] - "#{bamboo_url}/browse/#{result_key}" + URI.join(bamboo_url, "/browse/#{result_key}").to_s end end @@ -134,7 +134,7 @@ class BambooService < CiService return unless supported_events.include?(data[:object_kind]) # Bamboo requires a GET and does not take any data. - self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", - verify: false) + url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s + self.class.get(url, verify: false) end end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f9f0483876..6ab6d7417b 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -23,7 +23,7 @@ class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher boolean_accessor :notify_only_broken_builds - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? } def initialize_properties if properties.nil? @@ -87,10 +87,14 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',').compact.reject(&:blank?) + all_recipients = [] + + unless recipients.blank? + all_recipients += recipients.split(',').compact.reject(&:blank?) + end if add_pusher? && data[:user][:email] - all_recipients << "#{data[:user][:email]}" + all_recipients << data[:user][:email] end all_recipients diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e9416131..8dceee5e2c 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -85,13 +85,15 @@ class TeamcityService < CiService end def build_info(sha) - url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ - "branch:unspecified:any,number:#{sha}") + url = URI.join( + teamcity_url, + "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}" + ).to_s auth = { username: username, - password: password, + password: password } - @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end def build_page(sha, ref) @@ -100,12 +102,14 @@ class TeamcityService < CiService if @response.code != 200 # If actual build link can't be determined, # send user to build summary page. - "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" + URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s else # If actual build link is available, go to build result page. built_id = @response['build']['id'] - "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ - "&buildTypeId=#{build_type}" + URI.join( + teamcity_url, + "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" + ).to_s end end @@ -140,12 +144,13 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) - self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", - body: ""\ - ""\ - '', - headers: { 'Content-type' => 'application/xml' }, - basic_auth: auth - ) + self.class.post( + URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s, + body: ""\ + ""\ + '', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: auth + ) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 8dead3a588..0b2289cfa3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -253,6 +253,8 @@ class Repository # This ensures this particular cache is flushed after the first commit to a # new repository. expire_emptiness_caches if empty? + expire_branch_count_cache + expire_tag_count_cache end def expire_branch_cache(branch_name = nil) @@ -896,9 +898,9 @@ class Repository end def main_language - unless empty? - Linguist::Repository.new(rugged, rugged.head.target_id).language - end + return if empty? || rugged.head_unborn? + + Linguist::Repository.new(rugged, rugged.head.target_id).language end def avatar diff --git a/app/models/user.rb b/app/models/user.rb index 2b0bee2099..031315debd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -143,6 +143,7 @@ class User < ActiveRecord::Base has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy # # Validations @@ -157,7 +158,7 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true + validates :notification_level, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } @@ -190,6 +191,13 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + # + # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813 + # Because user.notification_disabled? is much better than user.disabled? + enum notification_level: [:disabled, :participating, :watch, :global, :mention] + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -349,10 +357,6 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def notification - @notification ||= Notification.new(self) - end - def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) @@ -827,6 +831,10 @@ class User < ActiveRecord::Base end end + def notification_settings_for(source) + notification_settings.find_or_initialize_by(source: source) + end + private def projects_union diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 770f32de94..772f5c5fff 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -3,7 +3,7 @@ module Issues def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) - issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) + issue_url = Gitlab::UrlBuilder.build(issue) issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ac5b58db86..e6837a1869 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,8 +20,7 @@ module MergeRequests def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action hook_data end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 6e9152e444..fa34753c4f 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -51,7 +51,7 @@ module MergeRequests # be interpreted as the use wants to close that issue on this project # Pattern example: 112-fix-mep-mep # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/-(\d+)\z/) + if match = merge_request.source_branch.match(/\A(\d+)-/) iid = match[1] closes_issue = "Closes ##{iid}" diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index b8e08c9f1e..3b90399af6 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -3,7 +3,7 @@ module Milestones def execute milestone = project.milestones.new(params) - if milestone.save + if milestone.save! event_service.open_milestone(milestone, current_user) end diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb new file mode 100644 index 0000000000..7f1b30ec84 --- /dev/null +++ b/app/services/notes/delete_service.rb @@ -0,0 +1,8 @@ +module Notes + class DeleteService < BaseService + def execute(note) + note.destroy + note.reset_events_cache + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index eff0d96f93..42ec1ac9e1 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -253,8 +253,8 @@ class NotificationService def project_watchers(project) project_members = project_member_notification(project) - users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) + users_with_project_level_global = project_member_notification(project, :global) + users_with_group_level_global = group_member_notification(project, :global) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) @@ -264,18 +264,16 @@ class NotificationService end def project_member_notification(project, notification_level=nil) - project_members = project.project_members - if notification_level - project_members.where(notification_level: notification_level).pluck(:user_id) + project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else - project_members.pluck(:user_id) + project.notification_settings.pluck(:user_id) end end def group_member_notification(project, notification_level) if project.group - project.group.group_members.where(notification_level: notification_level).pluck(:user_id) + project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else [] end @@ -284,13 +282,13 @@ class NotificationService def users_with_global_level_watch(ids) User.where( id: ids, - notification_level: Notification::N_WATCH + notification_level: NotificationSetting.levels[:watch] ).pluck(:id) end # Build a list of users based on project notifcation settings def select_project_member_setting(project, global_setting, users_global_level_watch) - users = project_member_notification(project, Notification::N_WATCH) + users = project_member_notification(project, :watch) # If project setting is global, add to watch list if global setting is watch global_setting.each do |user_id| @@ -304,7 +302,7 @@ class NotificationService # Build a list of users based on group notification settings def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) - uids = group_member_notification(project, Notification::N_WATCH) + uids = group_member_notification(project, :watch) # Group setting is watch, add to users list if user is not project member users = [] @@ -331,40 +329,46 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - reject_users(users, :disabled?, project) + reject_users(users, :disabled, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) - reject_users(users, :mention?, project) + reject_users(users, :mention, project) end - # Reject users which method_name from notification object returns true. + # Reject users which has certain notification level # # Example: - # reject_users(users, :watch?, project) + # reject_users(users, :watch, project) # - def reject_users(users, method_name, project = nil) + def reject_users(users, level, project = nil) + level = level.to_s + + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end + users = users.to_a.compact.uniq users = users.reject(&:blocked?) users.reject do |user| - next user.notification.send(method_name) unless project + next user.notification_level == level unless project - member = project.project_members.find_by(user_id: user.id) + setting = user.notification_settings_for(project) - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) + if !setting && project.group + setting = user.notification_settings_for(project.group) end - # reject users who globally set mention notification and has no membership - next user.notification.send(method_name) unless member + # reject users who globally set mention notification and has no setting per project/group + next user.notification_level == level unless setting # reject users who set mention notification in project - next true if member.notification.send(method_name) + next true if setting.level == level - # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.send(method_name) + # reject users who have mention level in project and disabled in global settings + setting.global? && user.notification_level == level end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index a0973c5d26..3b7c36f090 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -26,7 +26,9 @@ module Projects GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure - @project.update_column(:pushes_since_gc, 0) + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @project.update_column(:pushes_since_gc, 0) + end end def needed? @@ -34,14 +36,18 @@ module Projects end def increment! - @project.increment!(:pushes_since_gc) + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @project.increment!(:pushes_since_gc) + end end private def try_obtain_lease - lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) - lease.try_obtain + Gitlab::Metrics.measure(:obtain_housekeeping_lease) do + lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) + lease.try_obtain + end end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 2e73465446..79a27f4af7 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,8 +34,9 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - # Apply new namespace id + # Apply new namespace id and visibility level project.namespace = new_namespace + project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? project.save! # Notifications @@ -56,7 +57,7 @@ module Projects Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) project.old_path_with_namespace = old_path - + SystemHooksService.new.execute_hooks_for(project, :transfer) true end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 658b086496..82a0e2fd1f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -222,7 +222,7 @@ class SystemNoteService # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `issue-branch-button-201`" + # "Started branch `201-issue-branch-button`" def self.new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 3bc1b24b5e..06be1a5331 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -3,11 +3,9 @@ %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr = form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f| + = form_errors(@abuse_report) + = f.hidden_field :user_id - - if @abuse_report.errors.any? - .alert.alert-danger - - @abuse_report.errors.full_messages.each do |msg| - %p= msg .form-group = f.label :user_id, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 6f325914d1..d88f3ad314 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -1,8 +1,5 @@ = form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| - - if @appearance.errors.any? - .alert.alert-danger - - @appearance.errors.full_messages.each do |msg| - %p= msg + = form_errors(@appearance) %fieldset.sign-in %legend diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index de86dacbb1..a8cca1a81c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -1,9 +1,5 @@ = form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @application_setting.errors.any? - #error_explanation - .alert.alert-danger - - @application_setting.errors.full_messages.each do |msg| - %p= msg + = form_errors(@application_setting) %fieldset %legend Visibility and Access Controls diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index e18f7b499d..4aacbb8cd7 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,9 +1,6 @@ = form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - - application.errors.full_messages.each do |msg| - %p= msg + = form_errors(application) + = content_tag :div, class: 'form-group' do = f.label :name, class: 'col-sm-2 control-label' .col-sm-10 diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index b748460a9f..6b157abf84 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -4,10 +4,8 @@ = render_broadcast_message(@broadcast_message.message.presence || "Your message here") = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| - -if @broadcast_message.errors.any? - .alert.alert-danger - - @broadcast_message.errors.full_messages.each do |msg| - %p= msg + = form_errors(@broadcast_message) + .form-group = f.label :message, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 5b46b3222a..15aa059c93 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -4,11 +4,7 @@ %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| - -if @deploy_key.errors.any? - .alert.alert-danger - %ul - - @deploy_key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@deploy_key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 7f2b1cd235..0cc405401c 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,8 +1,5 @@ = form_for [:admin, @group], html: { class: "form-horizontal" } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f .form-group.group-description-holder diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 53b3cd04c6..ad952052f2 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -10,10 +10,8 @@ = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL:", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 3a78855822..112a201faf 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -1,9 +1,5 @@ = form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| - - if @identity.errors.any? - #error_explanation - .alert.alert-danger - - @identity.errors.full_messages.each do |msg| - %p= msg + = form_errors(@identity) .form-group = f.label :provider, class: 'control-label' diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 8c6b389bf1..448aa95354 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index d2527ede99..b05fdbd555 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,10 +1,6 @@ .user_new = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f| - -if @user.errors.any? - #error_explanation - .alert.alert-danger - - @user.errors.full_messages.each do |msg| - %p= msg + = form_errors(@user) %fieldset %legend Account diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 906b067615..5c98265727 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -1,9 +1,5 @@ = form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %ul - - application.errors.full_messages.each do |msg| - %li= msg + = form_errors(application) .form-group = f.label :name, class: 'label-light' diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 55f4a6f287..0aff79749e 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -68,7 +68,7 @@ %td= app.name %td= token.created_at %td= token.scopes - %td= render 'delete_form', application: app + %td= render 'doorkeeper/authorized_applications/delete_form', application: app - @authorized_anonymous_tokens.each do |token| %tr %td diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index ea5a035839..a698cbbe9d 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -5,9 +5,7 @@ Group settings .panel-body = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first + = form_errors(@group) = render 'shared/group_form', f: f .form-group diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index a8e1ed77da..7d9d27ae1f 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -8,8 +8,16 @@ This will create milestone in every selected project %hr -= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| .row + - if @milestone.errors.any? + #error_explanation + .alert.alert-danger + %ul + - @milestone.errors.full_messages.each do |msg| + %li + = msg + .col-md-6 .form-group = f.label :title, "Title", class: "control-label" @@ -19,7 +27,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert .form-group diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 30ab8aeba1..2b8bc269e6 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -6,10 +6,7 @@ %hr = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true .form-group.group-description-holder diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml new file mode 100644 index 0000000000..2ed51d87ca --- /dev/null +++ b/app/views/layouts/_collapse_button.html.haml @@ -0,0 +1,4 @@ +- 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 9be36273c7..c799e9c588 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,5 @@ .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 @@ -10,19 +8,15 @@ .gitlab-text-container %h3 GitLab - - 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}" + - if defined?(sidebar) && sidebar + = render "layouts/nav/#{sidebar}" + - elsif current_user + = render 'layouts/nav/dashboard' - else - = render "layouts/nav/#{primary_sidebar}" + = render 'layouts/nav/explore' .collapse-nav - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" + = render partial: 'layouts/collapse_button' - 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/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0f3b811937..3beb8ff7c0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -8,7 +8,7 @@ .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs - = render 'layouts/search' + = render 'layouts/search' unless current_controller?(:search) %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') @@ -45,6 +45,8 @@ %h1.title= title + = yield :header_content + = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 22d1d4d859..280a1b9372 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) do + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) 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 d1a180e429..5cef652da1 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -9,18 +9,18 @@ = icon('bell fw') %span Todos - %span.count= number_with_delimiter(todos_pending_count) + %span.count.todos-pending-count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do = icon('dashboard fw') %span Activity - = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do + = nav_link(controller: :groups) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups - = nav_link(path: 'dashboard#milestones') do + = nav_link(controller: :milestones) do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span @@ -48,6 +48,7 @@ %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 0b7de9633e..55940741dc 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,4 +1,12 @@ %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') @@ -34,7 +42,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link do + = nav_link(html_options: { class: "separate-item" }) 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 cc119fd64e..3b9d31a6fc 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,4 +1,12 @@ %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 d0f82b5f57..86b46e8c75 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,4 +1,19 @@ %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') @@ -98,7 +113,7 @@ Snippets - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class}"}) do + = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index a7ef31acd3..6dfe7fbdae 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -17,4 +17,12 @@ - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user +- content_for :header_content do + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + = dropdown_title("Go to a project") + = dropdown_filter("Search your projects") + = dropdown_content + = dropdown_loading + = render template: "layouts/application" diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 4d78215ed3..b3ed59a1a4 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f| - - if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :key, class: 'label-light' diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml new file mode 100644 index 0000000000..89ae7ffda2 --- /dev/null +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to group.name, group_path(group) + + .pull-right + = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml new file mode 100644 index 0000000000..17c097154d --- /dev/null +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to_project(project) + + .pull-right + = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml deleted file mode 100644 index d0d044136f..0000000000 --- a/app/views/profiles/notifications/_settings.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%li.notification-list-item - %span.notification.fa.fa-holder.append-right-5 - - if notification.global? - = notification_icon(@notification) - - else - = notification_icon(notification) - - %span.str-truncated - - if membership.kind_of? GroupMember - = link_to membership.group.name, membership.group - - else - = link_to_project(membership.project) - .pull-right - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') - = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 3d15c0d932..a2a505c082 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,8 +1,8 @@ - page_title "Notifications" - header_title page_title, profile_notifications_path -= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - -if @user.errors.any? +%div + - if @user.errors.any? %div.alert.alert-danger %ul - @user.errors.full_messages.each do |msg| @@ -20,56 +20,55 @@ .col-lg-9 %h5 Global notification settings - .form-group - = f.label :notification_email, class: "label-light" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" - .form-group - = f.label :notification_level, class: 'label-light' - .radio - = f.label :notification_level, value: Notification::N_DISABLED do - = f.radio_button :notification_level, Notification::N_DISABLED - .level-title - Disabled - %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_MENTION do - = f.radio_button :notification_level, Notification::N_MENTION - .level-title - On Mention - %p You will receive notifications only for comments in which you were @mentioned + = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| + .form-group + = f.label :notification_email, class: "label-light" + = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + .form-group + = f.label :notification_level, class: 'label-light' + .radio + = f.label :notification_level, value: :disabled do + = f.radio_button :notification_level, :disabled + .level-title + Disabled + %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_PARTICIPATING do - = f.radio_button :notification_level, Notification::N_PARTICIPATING - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) + .radio + = f.label :notification_level, value: :mention do + = f.radio_button :notification_level, :mention + .level-title + On Mention + %p You will receive notifications only for comments in which you were @mentioned - .radio - = f.label :notification_level, value: Notification::N_WATCH do - = f.radio_button :notification_level, Notification::N_WATCH - .level-title - Watch - %p You will receive notifications for any activity + .radio + = f.label :notification_level, value: :participating do + = f.radio_button :notification_level, :participating + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - .prepend-top-default - = f.submit 'Update settings', class: "btn btn-create" + .radio + = f.label :notification_level, value: :watch do + = f.radio_button :notification_level, :watch + .level-title + Watch + %p You will receive notifications for any activity + + .prepend-top-default + = f.submit 'Update settings', class: "btn btn-create" %hr -.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 + %h5 + Groups (#{@group_notifications.count}) + %div + %ul.bordered-list + - @group_notifications.each do |setting| + = render 'group_settings', setting: setting, group: setting.source + %h5 + Projects (#{@project_notifications.count}) + %p.account-well + To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there. + .append-bottom-default + %ul.bordered-list + - @project_notifications.each do |setting| + = render 'project_settings', setting: setting, project: setting.source diff --git a/app/views/profiles/notifications/update.js.haml b/app/views/profiles/notifications/update.js.haml deleted file mode 100644 index 84c6ab2559..0000000000 --- a/app/views/profiles/notifications/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @saved - :plain - new Flash("Notification settings saved", "notice") -- else - :plain - new Flash("Failed to save new settings", "alert") diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 44d758dceb..5ac8a8b9d0 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -13,11 +13,8 @@ - unless @user.password_automatically_set? or recover your current one = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f| - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + - unless @user.password_automatically_set? .form-group = f.label :current_password, class: 'label-light' diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index d165f758c8..2eb9fac57c 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -7,11 +7,8 @@ Please set a new password before proceeding. %br After a successful password update you will be redirected to login screen. - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + + = form_errors(@user) - unless @user.password_automatically_set? .form-group diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index dcb3be9585..f59d27f7ed 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,9 +1,6 @@ = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| - -if @user.errors.any? - %div.alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + .row .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml index 7c8bb33ed7..2dba22d3be 100644 --- a/app/views/projects/_errors.html.haml +++ b/app/views/projects/_errors.html.haml @@ -1,4 +1 @@ -- if @project.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - = @project.errors.full_messages.first += form_errors(@project) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 4920910fee..7a78d61a61 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -2,13 +2,13 @@ .md-header %ul.nav-links %li.active - %a.js-md-write-button{ href: "#md-write-holder" } + %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write %li - %a.js-md-preview-button{ href: "#md-preview-holder" } + %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' } + %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index bddff5cdcb..e1e3501396 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,8 +1,8 @@ .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + = f.text_area attr, class: classes, placeholder: placeholder - else - = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index a3786c35a1..c1e3e5b73a 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,20 +1,11 @@ -- case @membership -- when ProjectMember - = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do - = hidden_field_tag :notification_type, 'project' - = hidden_field_tag :notification_id, @membership.id - = hidden_field_tag :notification_level +- if @notification_setting + = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') - = notification_label(@membership) + = notification_title(@notification_setting.level) = icon('angle-down') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - Notification.project_notification_levels.each do |level| - = notification_list_item(level, @membership) - -- when GroupMember - .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} - = icon('bell') - = notification_label(@membership) - = icon('angle-down') + - NotificationSetting.levels.each do |level| + = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7f2903589a..7da8923124 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -19,24 +19,17 @@ .pull-right - if ci_commit = render_ci_status(ci_commit) -   = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - .notes_count - - if note_count > 0 - %span.light - %i.fa.fa-comments - = note_count - - if commit.description? .commit-row-description.js-toggle-content %pre = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) .commit-row-info + by = commit_author_link(commit, avatar: true, size: 24) - authored .committed_ago #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}   = link_to_browse_code(project, commit) diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 5e182af266..f6565f8583 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| - -if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 2e1a37aa06..eaab99973a 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs, diff_refs) -.content-block.oneline-block +.content-block.oneline-block.files-changed .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 698ed02ea0..83a8d7ae9b 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -3,7 +3,7 @@ - if diff_file.diff.submodule? %span = icon('archive fw') - %strong + %span = submodule_link(blob, @commit.id, project.repository) - else = blob_icon blob.mode, blob.name @@ -11,13 +11,13 @@ = link_to "#diff-#{i}" do - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.filename.old + .filename.old = old_path → - %strong.filename.new + .filename.new = new_path - else - %strong + %span = diff_file.new_path - if diff_file.deleted_file deleted @@ -28,8 +28,8 @@ .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file" do - = icon('comments') + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do + = icon('comment') \ - if editable_diff?(diff_file) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index d7c4906874..81948513e4 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,11 +14,11 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -27,11 +27,11 @@ - new_line_class = nil - new_line_code = left[:line_code] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 6d872cd0b2..76a4f41193 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -210,6 +210,7 @@ %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. .form-actions = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 67d016bd87..e39224d86c 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -9,10 +9,8 @@ %hr.clearfix = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 33c48199ba..7076f5db01 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue :javascript diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6fa059cbe6..5fe5ddc081 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -64,9 +64,11 @@ = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') - .merge-requests - = render 'merge_requests' - = render 'related_branches' + #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)} + // This element is filled in using JavaScript. + + #related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)} + // This element is filled in using JavaScript. .content-block.content-block-small = render 'new_branch' diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index be7a0bb562..aa143e54ff 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 0612863296..097a65969a 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,24 +1,27 @@ %li{id: dom_id(label)} = render "shared/label_row", label: label - .pull-right - %strong.append-right-20 + .pull-info-right + %span.append-right-20 = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'open merge request' + = pluralize label.open_merge_requests_count, 'merge request' - %strong.append-right-20 + %span.append-right-20 = link_to_label(label) do = pluralize label.open_issues_count(current_user), 'open issue' - if current_user .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .subscription-status{data: {status: label_subscription_status(label)}} - %button.btn.btn-sm.btn-info.subscribe-button + + %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}} %span= label_subscription_toggle_button_text(label) - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' - = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do + %i.fa.fa-pencil-square-o + = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do + %i.fa.fa-trash-o - if current_user :javascript diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 3e4ab09c6d..88525f4036 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 01dc7519be..7d7c487e97 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -5,33 +5,74 @@ .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 - .panel.panel-default + .panel.panel-default.panel-new-merge-request .panel-heading - %strong Source branch - .panel-body - = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true }) -   - = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } }) + Source branch + .panel-body.clearfix + .merge-request-select.dropdown + = f.hidden_field :source_project_id + = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" } + .dropdown-menu.dropdown-menu-selectable.dropdown-source-project + = dropdown_title("Select source project") + = dropdown_filter("Search projects") + = dropdown_content do + - is_active = f.object.source_project_id == @merge_request.source_project.id + %ul + %li + %a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } } + = @merge_request.source_project_path + .merge-request-select.dropdown + = f.hidden_field :source_branch + = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } + .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch + = dropdown_title("Select source branch") + = dropdown_filter("Search branches") + = dropdown_content do + %ul + - @merge_request.source_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } } + = branch .panel-footer - .mr_source_commit + = icon('spinner spin', class: 'js-source-loading') + %ul.list-unstyled.mr_source_commit .col-md-6 - .panel.panel-default + .panel.panel-default.panel-new-merge-request .panel-heading - %strong Target branch - .panel-body + Target branch + .panel-body.clearfix - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true }) -   - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } }) + .merge-request-select.dropdown + = f.hidden_field :target_project_id + = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } + .dropdown-menu.dropdown-menu-selectable.dropdown-target-project + = dropdown_title("Select target project") + = dropdown_filter("Search projects") + = dropdown_content do + %ul + - projects.each do |project| + %li + %a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } } + = project.path_with_namespace + .merge-request-select.dropdown + = f.hidden_field :target_branch + = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" } + .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown + = dropdown_title("Select target branch") + = dropdown_filter("Search branches") + = dropdown_content do + %ul + - @merge_request.target_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } } + = branch .panel-footer - .mr_target_commit + = icon('spinner spin', class: "js-target-loading") + %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? - .alert.alert-danger - - @merge_request.errors.full_messages.each do |msg| - %div= msg - + = form_errors(@merge_request) - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? .light-well.append-bottom-default .center @@ -45,40 +86,11 @@ and %span.label-branch #{@merge_request.target_branch} are the same. - - - .form-actions - = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" + = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript - var source_branch = $("#merge_request_source_branch") - , target_branch = $("#merge_request_target_branch") - , target_project = $("#merge_request_target_project_id"); - - $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: source_branch.val() }); - $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); - - target_project.on("change", function() { - $.get("#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: $(this).val() }); - }); - source_branch.on("change", function() { - $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: $(this).val() }); - $(".mr-compare-errors").fadeOut(); - $(".mr-compare-btn").enable(); - }); - target_branch.on("change", function() { - $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); - $(".mr-compare-errors").fadeOut(); - $(".mr-compare-btn").enable(); - }); - - -:javascript - $(".merge-request-form").on('submit', function () { - if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") { - $(".mr-compare-errors").html("You must select source and target branch to proceed"); - $(".mr-compare-errors").fadeIn(); - event.preventDefault(); - return; - } + new Compare({ + targetProjectUrl: "#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", + sourceBranchUrl: "#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", + targetBranchUrl: "#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}" }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9e59f7df71..2f14a91e64 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,7 +10,7 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request = f.hidden_field :source_project_id = f.hidden_field :source_branch diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/branch_from.html.haml new file mode 100644 index 0000000000..4f90dde6fa --- /dev/null +++ b/app/views/projects/merge_requests/branch_from.html.haml @@ -0,0 +1 @@ += commit_to_html(@commit, @source_project, false) diff --git a/app/views/projects/merge_requests/branch_from.js.haml b/app/views/projects/merge_requests/branch_from.js.haml deleted file mode 100644 index 9210798f39..0000000000 --- a/app/views/projects/merge_requests/branch_from.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -:plain - $(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}"); - $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/branch_to.html.haml new file mode 100644 index 0000000000..67a7a6bcec --- /dev/null +++ b/app/views/projects/merge_requests/branch_to.html.haml @@ -0,0 +1 @@ += commit_to_html(@commit, @target_project, false) diff --git a/app/views/projects/merge_requests/branch_to.js.haml b/app/views/projects/merge_requests/branch_to.js.haml deleted file mode 100644 index 32fe2d535f..0000000000 --- a/app/views/projects/merge_requests/branch_to.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -:plain - $(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}"); - $('.js-timeago').timeago() diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/update_branches.html.haml new file mode 100644 index 0000000000..1b93188a10 --- /dev/null +++ b/app/views/projects/merge_requests/update_branches.html.haml @@ -0,0 +1,5 @@ +%ul + - @target_branches.each do |branch| + %li + %a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } } + = branch diff --git a/app/views/projects/merge_requests/update_branches.js.haml b/app/views/projects/merge_requests/update_branches.js.haml deleted file mode 100644 index ca21b3bc0d..0000000000 --- a/app/views/projects/merge_requests/update_branches.js.haml +++ /dev/null @@ -1,9 +0,0 @@ -:plain - $(".target_branch").html("#{escape_javascript(options_for_select(@target_branches))}"); - - $('select.target_branch').select2({ - width: 'resolve', - dropdownAutoWidth: true - }); - - $(".mr_target_commit").html(""); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 92d9535893..3c68d61c4b 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -8,20 +8,22 @@ = render 'projects/merge_requests/widget/locked' :javascript - var merge_request_widget; var opts = { merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, 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_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, ci_enable: #{@project.ci_service ? "true" : "false"}, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; - if(typeof merge_request_widget === 'undefined') { - merge_request_widget = new MergeRequestWidget(opts); - } else { - merge_request_widget.setOpts(opts); - } + merge_request_widget = new MergeRequestWidget(opts); diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 23f2bca7ba..687222fa92 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,9 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| - -if @milestone.errors.any? - .alert.alert-danger - %ul - - @milestone.errors.full_messages.each do |msg| - %li= msg += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f| + = form_errors(@milestone) .row .col-md-6 .form-group @@ -14,7 +10,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/notes/hints' .clearfix .error-alert diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 2523311213..a4c6094c69 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -19,7 +19,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} .input-group-addon \/ - else diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 11f9859a90..39be072855 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -3,9 +3,6 @@ - if !defined?(line) || line == note.diff_line %tr.notes_holder %td.notes_line{ colspan: 2 } - %span.discussion-notes-count - %i.fa.fa-comment - = notes.count %td.notes_content %ul.notes{ data: { discussion_id: note.discussion_id } } = render notes diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index bb761ed2f9..f8aa5e2fa7 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -4,9 +4,6 @@ %tr.notes_holder - if note1 %td.notes_line.old - %span.btn.disabled - %i.fa.fa-comment - = notes_left.count %td.notes_content.parallel.old %ul.notes{ data: { discussion_id: note1.discussion_id } } = render notes_left @@ -19,9 +16,6 @@ - if note2 %td.notes_line.new - %span.btn.disabled - %i.fa.fa-comment - = notes_right.count %td.notes_content.parallel.new %ul.notes{ data: { discussion_id: note2.discussion_id } } = render notes_right diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 23e4f93eab..c87a3fadf7 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index c446ecec2c..d0ac380f21 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a681d6dece..03a44ca99c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -10,15 +10,15 @@ = "#{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 - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role - = access + .note-actions + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + - if note_editable?(note) = 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: 'note-action-button js-note-delete' do + = icon('pencil') + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index cfd7e1534c..653b02da4d 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -13,11 +13,7 @@ - if can? current_user, :admin_project, @project = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| - -if @protected_branch.errors.any? - .alert.alert-danger - %ul - - @protected_branch.errors.full_messages.each do |msg| - %li= msg + = form_errors(@protected_branch) .form-group = f.label :name, "Branch", class: 'control-label' diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index c4a3f06ee0..6f0b32aa16 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -9,11 +9,11 @@ %strong #{@tag.name} .prepend-top-default - = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f| + = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .error-alert - .form-actions.prepend-top-default - = f.submit 'Save changes', class: 'btn btn-save' - = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + .error-alert + .form-actions.prepend-top-default + = f.submit 'Save changes', class: 'btn btn-save' + = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 77c7c4d23d..b40a6e5cb2 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New Tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 @@ -30,9 +30,9 @@ = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'description form-control' + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. + .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index efe1e6f24c..ca284b84d3 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -13,13 +13,7 @@ = nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - - if @project.errors.any? - #error_explanation - %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg + = form_errors(@project) = f.fields_for :variables do |variable_form| .form-group diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index f0d1932e23..797a1a59e9 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| - -if @page.errors.any? - #error_explanation - .alert.alert-danger - - @page.errors.full_messages.each do |msg| - %p= msg += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| + = form_errors(@page) = f.hidden_field :title, value: @page.title .form-group @@ -15,7 +11,7 @@ = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/notes/hints' .clearfix diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 9544e3d3e1..d9400b1d9f 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,5 +1,5 @@ - project = note.project -- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- note_url = Gitlab::UrlBuilder.build(note) - noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 7afbaeddee..0a38327baa 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -6,7 +6,7 @@ .commit-message-container .max-width-marker = text_area_tag 'commit_message', - (params[:commit_message] || local_assigns[:text]), + (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 4b47b0291b..b38c5e18ef 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,4 +1,5 @@ %span.label-row - = link_to_label(label, tooltip: false) + %span.label-name + = link_to_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 5a60ff5a5d..fc935166bf 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -1,9 +1,4 @@ -- if @service.errors.any? - #error_explanation - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg += form_errors(@service) - if @service.help.present? .well diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index e2a9e5bfb9..aed2622a6d 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,10 +1,5 @@ -- if issuable.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - issuable.errors.full_messages.each do |msg| - %span= msg - %br += form_errors(issuable) + .form-group = f.label :title, class: 'control-label' .col-sm-10 @@ -34,7 +29,8 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .clearfix .error-alert @@ -53,10 +49,11 @@ .issue-assignee = f.label :assignee_id, "Assignee", class: 'control-label' .col-sm-10 - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) + .issuable-form-select-holder + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true, include_blank: true)   = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group @@ -64,8 +61,9 @@ = f.label :milestone_id, "Milestone", class: 'control-label' .col-sm-10 - if milestone_options(issuable).present? - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) + .issuable-form-select-holder + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - else .prepend-top-10 %span.light No open milestones available. @@ -73,13 +71,13 @@ - if can? current_user, :admin_milestone, issuable.project = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group + - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10 - - if issuable.project.labels.any? + .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } + - if has_labels = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else - .prepend-top-10 %span.light No labels yet.   - if can? current_user, :admin_label, issuable.project @@ -131,8 +129,6 @@ - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-grouped' do - = icon('trash-o') - Delete + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, + method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index fd5e58c1f1..f722e61eea 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,7 +1,7 @@ - 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"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{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"}} %span.dropdown-toggle-text = h(params[:label_name].presence || "Label") = icon('chevron-down') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 47e544acf5..94affa4b59 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,6 @@ %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar + - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header %span.issuable-count.hide-collapsed.pull-left = issuable.iid @@ -29,7 +30,7 @@ .title.hide-collapsed Assignee = icon('spinner spin', class: 'block-loading') - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - if issuable.assignee @@ -41,9 +42,10 @@ = issuable.assignee.to_reference - else %span.assign-yourself - No assignee - - %a.js-assign-yourself{ href: '#' } - assign yourself + No assignee + - if can_edit_issuable + %a.js-assign-yourself{ href: '#' } + \- assign yourself .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' @@ -60,7 +62,7 @@ .title.hide-collapsed Milestone = icon('spinner spin', class: 'block-loading') - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - if issuable.milestone @@ -82,7 +84,7 @@ .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - if issuable.labels.any? diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 868b235700..b15e8ea73f 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -4,15 +4,16 @@ %li %span.label-row - = link_to milestones_label_path(options) do - - render_colored_label(label, tooltip: false) - %span.prepend-left-10 + %span.label-name + = link_to milestones_label_path(options) do + - render_colored_label(label, tooltip: false) + %span.prepend-description-left = markdown(label.description, pipeline: :single_line) - .pull-right - %strong.issues-count + .pull-info-right + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'opened')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - %strong.issues-count + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'closed')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 1041eccd1d..47ec09f62c 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,10 +1,6 @@ .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| - - if @snippet.errors.any? - .alert.alert-danger - %ul - - @snippet.errors.full_messages.each do |msg| - %li= msg + = form_errors(@snippet) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 7f29918dba..1de71f37d1 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -7,4 +7,4 @@ '#{user_calendar_activities_path}' ); -.calendar-hint Summary of issues, merge requests and push events +.calendar-hint Summary of issues, merge requests, and push events diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 8ffcdc4a32..dc249155b9 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,7 +1,7 @@ .awards.votes-block - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}} - = emoji_icon(emoji) + %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} + = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter = notes.count diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 3cc232ef1a..9e1215b21a 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -40,7 +40,7 @@ class PostReceive if Gitlab::Git.tag_ref?(ref) GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref) - else + elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end end diff --git a/bin/setup b/bin/setup index acdb2c1389..6cb2d7f1e3 100755 --- a/bin/setup +++ b/bin/setup @@ -18,7 +18,7 @@ Dir.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system "bin/rake db:reset" puts "\n== Removing old logs and tempfiles ==" system "rm -f log/*" diff --git a/config/application.rb b/config/application.rb index 5a0ac70aa2..2e2ed48db0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,11 +4,9 @@ require 'rails/all' require 'devise' I18n.config.enforce_available_locales = false Bundler.require(:default, Rails.env) -require_relative '../lib/gitlab/redis_config' +require_relative '../lib/gitlab/redis' module Gitlab - REDIS_CACHE_NAMESPACE = 'cache:gitlab' - class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -69,8 +67,8 @@ module Gitlab end end - redis_config_hash = Gitlab::RedisConfig.redis_store_options - redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE + redis_config_hash = Gitlab::Redis.redis_store_options + redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/environments/production.rb b/config/environments/production.rb index 909526605a..a9d8ac4b6d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -21,6 +21,9 @@ Rails.application.configure do # Generate digests for assets URLs config.assets.digest = true + # Enable compression of compiled assets using gzip. + config.assets.compress = true + # Defaults to nil and saved in location specified by config.assets.prefix # config.assets.manifest = YOUR_PATH diff --git a/config/environments/test.rb b/config/environments/test.rb index f96ac6f975..a703c0934f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,6 +8,7 @@ Rails.application.configure do config.cache_classes = false # Configure static asset server for tests with Cache-Control for performance + config.assets.digest = false config.serve_static_files = true config.static_cache_control = "public, max-age=3600" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index ddee467b14..b28fc5c8e0 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -46,6 +46,15 @@ production: &base # # relative_url_root: /gitlab + # Trusted Proxies + # Customize if you have GitLab behind a reverse proxy which is running on a different machine. + # Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address. + trusted_proxies: + # Examples: + #- 192.168.1.0/24 + #- 192.168.2.1 + #- 2001:0db8::/32 + # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') # user: git @@ -80,7 +89,7 @@ production: &base # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. # Tip: you can test your closing pattern at http://rubular.com. - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' ## Default project features settings default_projects_features: @@ -156,6 +165,9 @@ production: &base stuck_ci_builds_worker: cron: "0 0 * * *" + # Remove outdated repository archives + repository_archive_cache_worker: + cron: "0 * * * *" # # 2. GitLab CI settings @@ -304,6 +316,13 @@ production: &base # (default: false) auto_link_saml_user: false + # Set different Omniauth providers as external so that all users creating accounts + # via these providers will not be able to have access to internal projects. You + # will need to use the full name of the provider, like `google_oauth2` for Google. + # Refer to the examples below for the full names of the supported providers. + # (default: []) + external_providers: [] + ## Auth providers # Uncomment the following lines and fill in the data of the auth provider you want to use # If your favorite auth provider is not listed you can use others: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 2b98901527..287f99c724 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -129,6 +129,7 @@ Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? +Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? @@ -176,7 +177,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled']. Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 @@ -190,6 +191,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] +Settings.gitlab['trusted_proxies'] ||= [] # @@ -239,6 +241,9 @@ Settings['cron_jobs'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker' +Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *' +Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker' # diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 3e1deb8d30..22fe51a453 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -1,4 +1,5 @@ if Gitlab::Metrics.enabled? + require 'pathname' require 'influxdb' require 'connection_pool' require 'method_source' @@ -7,6 +8,7 @@ if Gitlab::Metrics.enabled? # ActiveSupport. require 'gitlab/metrics/subscribers/action_view' require 'gitlab/metrics/subscribers/active_record' + require 'gitlab/metrics/subscribers/rails_cache' Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) @@ -74,6 +76,37 @@ if Gitlab::Metrics.enabled? config.instrument_methods(const) config.instrument_instance_methods(const) end + + # Instruments all Banzai filters + Dir[Rails.root.join('lib', 'banzai', 'filter', '*.rb')].each do |file| + klass = File.basename(file, File.extname(file)).camelize + const = Banzai::Filter.const_get(klass) + + config.instrument_methods(const) + config.instrument_instance_methods(const) + end + + config.instrument_methods(Banzai::Renderer) + config.instrument_methods(Banzai::Querying) + + [Issuable, Mentionable, Participable].each do |klass| + config.instrument_instance_methods(klass) + config.instrument_instance_methods(klass::ClassMethods) + end + + config.instrument_methods(Gitlab::ReferenceExtractor) + config.instrument_instance_methods(Gitlab::ReferenceExtractor) + + # Instrument all service classes + services = Rails.root.join('app', 'services') + + Dir[services.join('**', '*.rb')].each do |file_path| + path = Pathname.new(file_path).relative_path_from(services) + const = path.to_s.sub('.rb', '').camelize.constantize + + config.instrument_methods(const) + config.instrument_instance_methods(const) + end end GC::Profiler.enable diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 3da5d46be9..88cb859871 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,8 +13,8 @@ end if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else - redis_config = Gitlab::RedisConfig.redis_store_options - redis_config[:namespace] = 'session:gitlab' + redis_config = Gitlab::Redis.redis_store_options + redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index cc83137745..f1eec67488 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,9 +1,7 @@ -SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' - Sidekiq.configure_server do |config| config.redis = { - url: Gitlab::RedisConfig.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + url: Gitlab::Redis.url, + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } config.server_middleware do |chain| @@ -29,7 +27,7 @@ end Sidekiq.configure_client do |config| config.redis = { - url: Gitlab::RedisConfig.url, - namespace: SIDEKIQ_REDIS_NAMESPACE + url: Gitlab::Redis.url, + namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE } end diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb new file mode 100644 index 0000000000..b8cc025bae --- /dev/null +++ b/config/initializers/trusted_proxies.rb @@ -0,0 +1,2 @@ +Rails.application.config.action_dispatch.trusted_proxies = + [ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies) diff --git a/config/mail_room.yml b/config/mail_room.yml index 60257329f3..761a32adb9 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,7 +2,7 @@ <% require "yaml" require "json" -require_relative "lib/gitlab/redis_config" +require_relative "lib/gitlab/redis" rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" @@ -18,7 +18,7 @@ if File.exists?(config_file) config['mailbox'] = "inbox" if config['mailbox'].nil? if config['enabled'] && config['address'] - redis_url = Gitlab::RedisConfig.new(rails_env).url + redis_url = Gitlab::Redis.new(rails_env).url %> - :host: <%= config['host'].to_json %> diff --git a/config/routes.rb b/config/routes.rb index 842fbb9984..688b83d2c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,6 +406,7 @@ Rails.application.routes.draw do resource :avatar, only: [:destroy] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resource :notification_setting, only: [:update] end end @@ -607,6 +608,7 @@ Rails.application.routes.draw do resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] + resource :notification_setting, only: [:update] resources :refs, only: [] do collection do @@ -699,6 +701,8 @@ Rails.application.routes.draw do resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + get :referenced_merge_requests + get :related_branches end collection do post :bulk_update diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb index e028ac82ba..540e4e6825 100644 --- a/db/fixtures/development/07_milestones.rb +++ b/db/fixtures/development/07_milestones.rb @@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do milestone_params = { title: "v#{i}.0", description: FFaker::Lorem.sentence, - state: ['opened', 'closed'].sample, + state: [:active, :closed].sample, } milestone = Milestones::CreateService.new( diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb index fe139e32ea..56c9a31ee3 100644 --- a/db/migrate/20130315124931_user_color_scheme.rb +++ b/db/migrate/20130315124931_user_color_scheme.rb @@ -1,7 +1,9 @@ class UserColorScheme < ActiveRecord::Migration + include Gitlab::Database + def up add_column :users, :color_scheme_id, :integer, null: false, default: 1 - User.where(dark_scheme: true).update_all(color_scheme_id: 2) + execute("UPDATE users SET color_scheme_id = 2 WHERE dark_scheme = #{true_value}") remove_column :users, :dark_scheme end diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb index 2a036bd999..85e31608d7 100644 --- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb +++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb @@ -3,14 +3,16 @@ class AddLastActivityColumnIntoProject < ActiveRecord::Migration add_column :projects, :last_activity_at, :datetime add_index :projects, :last_activity_at - Project.find_each do |project| - last_activity_date = if project.last_activity - project.last_activity.created_at - else - project.updated_at - end + select_all('SELECT id, updated_at FROM projects').each do |project| + project_id = project['id'] + update_date = project['updated_at'] + event = select_one("SELECT created_at FROM events WHERE project_id = #{project_id} ORDER BY created_at DESC LIMIT 1") - project.update_attribute(:last_activity_at, last_activity_date) + if event && event['created_at'] + update_date = event['created_at'] + end + + execute("UPDATE projects SET last_activity_at = '#{update_date}' WHERE id = #{project_id}") end end diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb index cf1e9f912a..89421cbeda 100644 --- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb +++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb @@ -1,13 +1,15 @@ class AddVisibilityLevelToProjects < ActiveRecord::Migration + include Gitlab::Database + def self.up add_column :projects, :visibility_level, :integer, :default => 0, :null => false - Project.where(public: true).update_all(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + execute("UPDATE projects SET visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} WHERE public = #{true_value}") remove_column :projects, :public end def self.down add_column :projects, :public, :boolean, :default => false, :null => false - Project.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).update_all(public: true) + execute("UPDATE projects SET public = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}") remove_column :projects, :visibility_level end end diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb index f4392c0f05..0a9f73a575 100644 --- a/db/migrate/20140313092127_migrate_already_imported_projects.rb +++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb @@ -1,12 +1,14 @@ class MigrateAlreadyImportedProjects < ActiveRecord::Migration + include Gitlab::Database + def up - Project.where(imported: true).update_all(import_status: "finished") - Project.where(imported: false).update_all(import_status: "none") + execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}") + execute("UPDATE projects SET import_status = 'none' WHERE imported = #{false_value}") remove_column :projects, :imported end def down add_column :projects, :imported, :boolean, default: false - Project.where(import_status: 'finished').update_all(imported: true) + execute("UPDATE projects SET imported = #{true_value} WHERE import_status = 'finished'") end end diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb index 7f125acb5d..93826185e8 100644 --- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -1,9 +1,11 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration + include Gitlab::Database + def up add_column :snippets, :visibility_level, :integer, :default => 0, :null => false - Snippet.where(private: true).update_all(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - Snippet.where(private: false).update_all(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::PRIVATE} WHERE private = #{true_value}") + execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::INTERNAL} WHERE private = #{false_value}") add_index :snippets, :visibility_level @@ -12,10 +14,10 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration def down add_column :snippets, :private, :boolean, :default => false, :null => false - - Snippet.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).update_all(private: false) - Snippet.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).update_all(private: true) - + + execute("UPDATE snippets SET private = #{false_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::INTERNAL}") + execute("UPDATE snippets SET private = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PRIVATE}") + remove_column :snippets, :visibility_level end end diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb new file mode 100644 index 0000000000..4755da8b80 --- /dev/null +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -0,0 +1,11 @@ +class CreateNotificationSettings < ActiveRecord::Migration + def change + create_table :notification_settings do |t| + t.references :user, null: false + t.references :source, polymorphic: true, null: false + t.integer :level, default: 0, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb new file mode 100644 index 0000000000..0a11086902 --- /dev/null +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -0,0 +1,17 @@ +# This migration will create one row of NotificationSetting for each Member row +# It can take long time on big instances. +# +# This migration can be done online but with following effects: +# - during migration some users will receive notifications based on their global settings (project/group settings will be ignored) +# - its possible to get duplicate records for notification settings since we don't create uniq index yet +# +class MigrateNewNotificationSetting < ActiveRecord::Migration + def up + timestamp = Time.now + execute "INSERT INTO notification_settings ( user_id, source_id, source_type, level, created_at, updated_at ) SELECT user_id, source_id, source_type, notification_level, '#{timestamp}', '#{timestamp}' FROM members WHERE user_id IS NOT NULL" + end + + def down + execute "DELETE FROM notification_settings" + end +end diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb new file mode 100644 index 0000000000..8aebce0244 --- /dev/null +++ b/db/migrate/20160328121138_add_notification_setting_index.rb @@ -0,0 +1,6 @@ +class AddNotificationSettingIndex < ActiveRecord::Migration + def change + add_index :notification_settings, :user_id + add_index :notification_settings, [:source_id, :source_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index df4c65a062..4970a07b72 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: 20160331133914) do +ActiveRecord::Schema.define(version: 20160331223143) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,7 +44,6 @@ ActiveRecord::Schema.define(version: 20160331133914) do t.datetime "updated_at" t.string "home_page_url" t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true t.text "restricted_visibility_levels" t.boolean "version_check_enabled", default: true t.integer "max_attachment_size", default: 10, null: false @@ -638,6 +637,18 @@ ActiveRecord::Schema.define(version: 20160331133914) do add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree + create_table "notification_settings", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "source_id", null: false + t.string "source_type", null: false + t.integer "level", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree + create_table "oauth_access_grants", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false diff --git a/doc/README.md b/doc/README.md index 724c7cca0f..d266093065 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. -- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples. +- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples. - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). diff --git a/doc/api/README.md b/doc/api/README.md index 7629ef294a..3a8fa6cebd 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -108,6 +108,7 @@ The following table shows the possible return codes for API requests. | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | +| `304 Not Modified` | Indicates that the resource has not been modified since the last request. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | | `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. | | `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. | diff --git a/doc/api/groups.md b/doc/api/groups.md index d1b5c9f5f0..2821bc21b8 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -126,6 +126,87 @@ Parameters: - `id` (required) - The ID or path of a group - `project_id` (required) - The ID of a project +## Update group + +Updates the project group. Only available to group owners and administrators. + +``` +PUT /groups/:id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the group | +| `name` | string | no | The name of the group | +| `path` | string | no | The path of the group | +| `description` | string | no | The description of the group | +| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | + +```bash +curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" + +``` + +Example response: + +```json +{ + "id": 5, + "name": "Experimental", + "path": "h5bp", + "description": "foo", + "visibility_level": 10, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/h5bp", + "projects": [ + { + "id": 9, + "description": "foo", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git", + "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git", + "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "Experimental / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": true, + "created_at": "2016-04-05T21:40:50.169Z", + "last_activity_at": "2016-04-06T16:52:08.432Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "Experimental", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-04-05T21:40:49.152Z", + "updated_at": "2016-04-07T08:07:48.466Z", + "description": "foo", + "avatar": { + "url": null + }, + "share_with_group_lock": false, + "visibility_level": 10 + }, + "avatar_url": null, + "star_count": 1, + "forks_count": 0, + "open_issues_count": 3, + "public_builds": true + } + ] +} +``` + ## Remove group Removes group with all projects inside. diff --git a/doc/api/issues.md b/doc/api/issues.md index cc6355d34e..3e78149f44 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -76,8 +76,9 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [] - }, + "labels" : [], + "subscribed" : false + } ] ``` @@ -152,7 +153,8 @@ Example response: "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", - "created_at" : "2016-01-04T15:31:46.176Z" + "created_at" : "2016-01-04T15:31:46.176Z", + "subscribed" : false } ] ``` @@ -213,7 +215,8 @@ Example response: "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", - "created_at" : "2016-01-04T15:31:46.176Z" + "created_at" : "2016-01-04T15:31:46.176Z", + "subscribed": false } ``` @@ -267,7 +270,8 @@ Example response: }, "description" : null, "updated_at" : "2016-01-07T12:44:33.959Z", - "milestone" : null + "milestone" : null, + "subscribed" : true } ``` @@ -294,6 +298,7 @@ PUT /projects/:id/issues/:issue_id | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | +| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close @@ -323,7 +328,8 @@ Example response: ], "id" : 85, "assignee" : null, - "milestone" : null + "milestone" : null, + "subscribed" : true } ``` @@ -346,6 +352,170 @@ DELETE /projects/:id/issues/:issue_id curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85 ``` +## Move an issue + +Moves an issue to a different project. If the operation is successful, a status +code `201` together with moved issue is returned. If the project, issue, or +target project is not found, error `404` is returned. If the target project +equals the source project or the user has insufficient permissions to move an +issue, error `400` together with an explaining error message is returned. + +``` +POST /projects/:id/issues/:issue_id/move +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `to_project_id` | integer | yes | The ID of the new project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + +## Subscribe to an issue + +Subscribes the authenticated user to an issue to receive notifications. If the +operation is successful, status code `201` together with the updated issue is +returned. If the user is already subscribed to the issue, the status code `304` +is returned. If the project or issue is not found, status code `404` is +returned. + +``` +POST /projects/:id/issues/:issue_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +``` + +Example response: + +```json +{ + "id": 92, + "iid": 11, + "project_id": 5, + "title": "Sit voluptas tempora quisquam aut doloribus et.", + "description": "Repellat voluptas quibusdam voluptatem exercitationem.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.652Z", + "updated_at": "2016-04-07T12:20:17.596Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/axel.block" + }, + "author": { + "name": "Kris Steuber", + "username": "solon.cremin", + "id": 10, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/solon.cremin" + } +} +``` + +## Unsubscribe from an issue + +Unsubscribes the authenticated user from the issue to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated issue is returned. If the user is not subscribed to the issue, the +status code `304` is returned. If the project or issue is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/issues/:issue_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription +``` + +Example response: + +```json +{ + "id": 93, + "iid": 12, + "project_id": 5, + "title": "Incidunt et rerum ea expedita iure quibusdam.", + "description": "Et cumque architecto sed aut ipsam.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.217Z", + "updated_at": "2016-04-07T13:02:37.905Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Edwardo Grady", + "username": "keyon", + "id": 21, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/keyon" + }, + "author": { + "name": "Vivian Hermann", + "username": "orville", + "id": 11, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "http://lgitlab.example.com/u/orville" + }, + "subscribed": false +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/labels.md b/doc/api/labels.md index 544e898b6a..3730c07c5a 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -23,42 +23,42 @@ Example response: { "name" : "bug", "color" : "#d9534f", - "description": "Bug reported by user" + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1 }, { "color" : "#d9534f", "name" : "confirmed", - "description": "Confirmed issue" + "description": "Confirmed issue", + "open_issues_count": 2, + "closed_issues_count": 5, + "open_merge_requests_count": 0 }, { "name" : "critical", "color" : "#d9534f", - "description": "Criticalissue. Need fix ASAP" - }, - { - "color" : "#428bca", - "name" : "discussion", - "description": "Issue that needs further discussion" + "description": "Criticalissue. Need fix ASAP", + "open_issues_count": 1, + "closed_issues_count": 3, + "open_merge_requests_count": 1 }, { "name" : "documentation", "color" : "#f0ad4e", - "description": "Issue about documentation" + "description": "Issue about documentation", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 2 }, { "color" : "#5cb85c", "name" : "enhancement", - "description": "Enhancement proposal" - }, - { - "color" : "#428bca", - "name" : "suggestion", - "description": "Suggestion" - }, - { - "color" : "#f0ad4e", - "name" : "support", - "description": "Support issue" + "description": "Enhancement proposal", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1 } ] ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index b20a6300b7..2057f9d77a 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -66,7 +66,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : false } ] ``` @@ -128,7 +129,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -227,6 +229,7 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", + "subscribed" : true, "changes": [ { "old_path": "VERSION", @@ -304,7 +307,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -373,7 +377,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -466,7 +471,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -530,7 +536,8 @@ Parameters: "due_date": null }, "merge_when_build_succeeds": true, - "merge_status": "can_be_merged" + "merge_status": "can_be_merged", + "subscribed" : true } ``` @@ -599,3 +606,151 @@ Example response: }, ] ``` + +## Subscribe to a merge request + +Subscribes the authenticated user to a merge request to receive notification. If +the operation is successful, status code `201` together with the updated merge +request is returned. If the user is already subscribed to the merge request, the +status code `304` is returned. If the project or merge request is not found, +status code `404` is returned. + +``` +POST /projects/:id/merge_requests/:merge_request_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": true +} +``` + +## Unsubscribe from a merge request + +Unsubscribes the authenticated user from a merge request to not receive +notifications from that merge request. If the operation is successful, status +code `200` together with the updated merge request is returned. If the user is +not subscribed to the merge request, the status code `304` is returned. If the +project or merge request is not found, status code `404` is returned. + +``` +DELETE /projects/:id/merge_requests/:merge_request_id/subscription +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription +``` + +Example response: + +```json +{ + "id": 17, + "iid": 1, + "project_id": 5, + "title": "Et et sequi est impedit nulla ut rem et voluptatem.", + "description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.", + "state": "opened", + "created_at": "2016-04-05T21:42:23.233Z", + "updated_at": "2016-04-05T22:11:52.900Z", + "target_branch": "ui-dev-kit", + "source_branch": "version-1-9", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Eileen Skiles", + "username": "leila", + "id": 19, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/leila" + }, + "assignee": { + "name": "Celine Wehner", + "username": "carli", + "id": 16, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/carli" + }, + "source_project_id": 5, + "target_project_id": 5, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 7, + "iid": 1, + "project_id": 5, + "title": "v2.0", + "description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.", + "state": "closed", + "created_at": "2016-04-05T21:41:40.905Z", + "updated_at": "2016-04-05T21:41:40.905Z", + "due_date": null + }, + "merge_when_build_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": false +} +``` diff --git a/doc/api/notes.md b/doc/api/notes.md index d4d63e825a..7aa1c2155b 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -32,6 +32,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T09:22:45Z", + "updated_at": "2013-10-02T10:22:45Z", "system": true, "upvote": false, "downvote": false, @@ -51,6 +52,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T09:56:03Z", + "updated_at": "2013-10-02T09:56:03Z", "system": true, "upvote": false, "downvote": false, @@ -87,6 +89,7 @@ Parameters: - `id` (required) - The ID of a project - `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note +- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ### Modify existing issue note @@ -103,6 +106,53 @@ Parameters: - `note_id` (required) - The ID of a note - `body` (required) - The content of a note +### Delete an issue note + +Deletes an existing note of an issue. On success, this API method returns 200 +and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/issues/:issue_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of an issue | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 +``` + +Example Response: + +```json +{ + "id": 636, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-05T22:10:44.164Z", + "system": false, + "noteable_id": 11, + "noteable_type": "Issue", + "upvote": false, + "downvote": false +} +``` + ## Snippets ### List all snippet notes @@ -180,6 +230,53 @@ Parameters: - `note_id` (required) - The ID of a note - `body` (required) - The content of a note +### Delete a snippet note + +Deletes an existing note of a snippet. On success, this API method returns 200 +and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/snippets/:snippet_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `snippet_id` | integer | yes | The ID of a snippet | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 +``` + +Example Response: + +```json +{ + "id": 1659, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-06T16:51:53.239Z", + "system": false, + "noteable_id": 52, + "noteable_type": "Snippet", + "upvote": false, + "downvote": false +} +``` + ## Merge Requests ### List all merge request notes @@ -223,6 +320,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T08:57:14Z", + "updated_at": "2013-10-02T08:57:14Z", "system": false, "upvote": false, "downvote": false, @@ -259,3 +357,50 @@ Parameters: - `merge_request_id` (required) - The ID of a merge request - `note_id` (required) - The ID of a note - `body` (required) - The content of a note + +### Delete a merge request note + +Deletes an existing note of a merge request. On success, this API method returns +200 and the deleted note. If the note does not exist, the API returns 404. + +``` +DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a merge request | +| `note_id` | integer | yes | The ID of a note | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 +``` + +Example Response: + +```json +{ + "id": 1602, + "body": "This is a good idea.", + "attachment": null, + "author": { + "id": 1, + "username": "pipin", + "email": "admin@example.com", + "name": "Pip", + "state": "active", + "created_at": "2013-09-30T13:46:01Z", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/u/pipin" + }, + "created_at": "2016-04-05T22:11:59.923Z", + "system": false, + "noteable_id": 7, + "noteable_type": "MergeRequest", + "upvote": false, + "downvote": false +} +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 3a909a2bc8..de1faadebf 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,132 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Star a project + +Stars a given project. Returns status code `201` and the project on success and +`304` if the project is already starred. + +``` +POST /projects/:id/star +``` + +| 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/5/star" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "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" + ], + "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" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 1 +} +``` + +### Unstar a project + +Unstars a given project. Returns status code `200` and the project on success +and `304` if the project is not starred. + +``` +DELETE /projects/:id/star +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 10, + "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" + ], + "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" + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0 +} +``` + ### Archive a project Archives the project if the user is either admin or the project owner of this project. This action is @@ -780,8 +906,10 @@ Parameters: - `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project - `user_id` (required) - The ID of a team member -This method is idempotent and can be called multiple times with the same parameters. -Revoking team membership for a user who is not currently a team member is considered success. +This method removes the project member if the user has the proper access rights to do so. +It returns a status code 403 if the member does not have the proper rights to perform this action. +In all other cases this method is idempotent and revoking team membership for a user who is not +currently a team member is considered success. Please note that the returned JSON currently differs slightly. Thus you should not rely on the returned JSON structure. diff --git a/doc/api/tags.md b/doc/api/tags.md index 17d12e9cc6..ac9fac92f4 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -38,6 +38,50 @@ Parameters: ] ``` +## Get a single repository tag + +Get a specific repository tag determined by its name. It returns `200` together +with the tag information if the tag exists. It returns `404` if the tag does not +exist. + +``` +GET /projects/:id/repository/tags/:tag_name +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `tag_name` | string | yes | The name of the tag | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0 +``` + +Example Response: + +```json +{ + "name": "v5.0.0", + "message": null, + "commit": { + "id": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "message": "v5.0.0\n", + "parent_ids": [ + "f61c062ff8bcbdb00e0a1b3317a91aed6ceee06b" + ], + "authored_date": "2015-02-01T21:56:31.000+01:00", + "author_name": "Arthur Verschaeve", + "author_email": "contact@arthurverschaeve.be", + "committed_date": "2015-02-01T21:56:31.000+01:00", + "committer_name": "Arthur Verschaeve", + "committer_email": "contact@arthurverschaeve.be" + }, + "release": null +} +``` + ## Create a new tag Creates a new tag in the repository that points to the supplied ref. @@ -148,4 +192,4 @@ Parameters: "tag_name": "1.0.0", "description": "Amazing release. Wow" } -``` \ No newline at end of file +``` diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md index 71db5aa5dc..9553bb11e9 100644 --- a/doc/ci/build_artifacts/README.md +++ b/doc/ci/build_artifacts/README.md @@ -1,7 +1,10 @@ # Introduction to build artifacts Artifacts is a list of files and directories which are attached to a build -after it completes successfully. +after it completes successfully. This feature is enabled by default in all GitLab installations. + +_If you are searching for ways to use artifacts, jump to +[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._ Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by GitLab Runner are uploaded to GitLab and are downloadable as a single archive @@ -16,13 +19,9 @@ The artifacts browser will be available only for new artifacts that are sent to GitLab using GitLab Runner version 1.0 and up. It will not be possible to browse old artifacts already uploaded to GitLab. -## Enabling build artifacts +## Disabling build artifacts -_If you are searching for ways to use artifacts, jump to -[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._ - -The artifacts feature is enabled by default in all GitLab installations. -To disable it site-wide, follow the steps below. +To disable artifacts site-wide, follow the steps below. --- diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 210f9c3e84..7f825e6a06 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,7 +30,7 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../ssh/README.md). +instructions to [generate an SSH key](../../ssh/README.md). Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` @@ -57,13 +57,13 @@ before_script: # WARNING: Use this only with the Docker executor, if you use it with shell # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` + - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' ``` As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). That's it! You can now have access to private servers or repositories in your build environment. @@ -79,12 +79,12 @@ on, and use that key for all projects that are run on this machine. First, you need to login to the server that runs your builds. Then from the terminal login as the `gitlab-runner` user and generate the SSH -key pair as described in the [SSH keys documentation](../ssh/README.md). +key pair as described in the [SSH keys documentation](../../ssh/README.md). As a final step, add the _public_ key from the one you created earlier to the services that you want to have an access to from within the build environment. If you are accessing a private GitLab repository you need to add it as a -[deploy key](../ssh/README.md#deploy-keys). +[deploy key](../../ssh/README.md#deploy-keys). Once done, try to login to the remote server in order to accept the fingerprint: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7da9b31e30..abb6e97e5e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1,5 +1,47 @@ # Configuration of your builds with .gitlab-ci.yml +This document describes the usage of `.gitlab-ci.yml`, the file that is used by +GitLab Runner to manage your project's builds. + +If you want a quick introduction to GitLab CI, follow our +[quick start guide](../quick_start/README.md). + +--- + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [.gitlab-ci.yml](#gitlab-ci-yml) + - [image and services](#image-and-services) + - [before_script](#before_script) + - [stages](#stages) + - [types](#types) + - [variables](#variables) + - [cache](#cache) + - [cache:key](#cache-key) +- [Jobs](#jobs) + - [script](#script) + - [stage](#stage) + - [only and except](#only-and-except) + - [tags](#tags) + - [when](#when) + - [artifacts](#artifacts) + - [artifacts:name](#artifacts-name) + - [dependencies](#dependencies) +- [Hidden jobs](#hidden-jobs) +- [Special YAML features](#special-yaml-features) + - [Anchors](#anchors) +- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml) +- [Skipping builds](#skipping-builds) +- [Examples](#examples) + + + +--- + +## .gitlab-ci.yml + From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root of your repository and contains definitions of how your project should be built. @@ -23,12 +65,10 @@ Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository. Jobs are used to create builds, which are then picked up by -[runners](../runners/README.md) and executed within the environment of the -runner. What is important, is that each job is run independently from each +[Runners](../runners/README.md) and executed within the environment of the +Runner. What is important, is that each job is run independently from each other. -## .gitlab-ci.yml - The YAML syntax allows for using more complex job specifications than in the above example: @@ -71,7 +111,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: This allows to specify a custom Docker image and a list of services that can be used for time of the build. The configuration of this feature is covered in -separate document: [Use Docker](../docker/README.md). +[a separate document](../docker/README.md). ### before_script @@ -86,7 +126,8 @@ The specification of `stages` allows for having flexible multi stage pipelines. The ordering of elements in `stages` defines the ordering of builds' execution: 1. Builds of the same stage are run in parallel. -1. Builds of next stage are run after success. +1. Builds of the next stage are run after the jobs from the previous stage + complete successfully. Let's consider the following example, which defines 3 stages: @@ -98,9 +139,9 @@ stages: ``` 1. First all jobs of `build` are executed in parallel. -1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel. -1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel. -1. If all jobs of `deploy` succeeds, the commit is marked as `success`. +1. If all jobs of `build` succeed, the `test` jobs are executed in parallel. +1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel. +1. If all jobs of `deploy` succeed, the commit is marked as `success`. 1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. @@ -278,14 +319,14 @@ job_name: | Keyword | Required | Description | |---------------|----------|-------------| -| script | yes | Defines a shell script which is executed by runner | +| script | yes | Defines a shell script which is executed by Runner | | image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a build stage (default: `test`) | | type | no | Alias for `stage` | | only | no | Defines a list of git refs for which build is created | | except | no | Defines a list of git refs for which build is not created | -| tags | no | Defines a list of tags which are used to select runner | +| tags | no | Defines a list of tags which are used to select Runner | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| @@ -294,7 +335,7 @@ job_name: ### script -`script` is a shell script which is executed by the runner. For example: +`script` is a shell script which is executed by the Runner. For example: ```yaml job: @@ -375,13 +416,13 @@ except master. ### tags -`tags` is used to select specific runners from the list of all runners that are +`tags` is used to select specific Runners from the list of all Runners that are allowed to run this project. -During the registration of a runner, you can specify the runner's tags, for +During the registration of a Runner, you can specify the Runner's tags, for example `ruby`, `postgres`, `development`. -`tags` allow you to run builds with runners that have the specified tags +`tags` allow you to run builds with Runners that have the specified tags assigned to them: ```yaml @@ -391,7 +432,7 @@ job: - postgres ``` -The specification above, will make sure that `job` is built by a runner that +The specification above, will make sure that `job` is built by a Runner that has both `ruby` AND `postgres` tags defined. ### when diff --git a/doc/development/README.md b/doc/development/README.md index 8940b558fb..3f3ef068f9 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -2,6 +2,8 @@ - [Architecture](architecture.md) of GitLab - [CI setup](ci_setup.md) for testing GitLab +- [Code review guidelines](code_review.md) for reviewing code and having code + reviewed. - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) - [Instrumentation](instrumentation.md) @@ -10,4 +12,5 @@ - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [SQL guidelines](sql.md) for SQL guidelines +- [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/code_review.md b/doc/development/code_review.md new file mode 100644 index 0000000000..40ae55ab90 --- /dev/null +++ b/doc/development/code_review.md @@ -0,0 +1,78 @@ +# Code Review Guidelines + +This guide contains advice and best practices for performing code review, and +having your code reviewed. + +All merge requests for GitLab CE and EE, whether written by a GitLab team member +or a volunteer contributor, must go through a code review process to ensure the +code is effective, understandable, and maintainable. + +Any developer can, and is encouraged to, perform code review on merge requests +of colleagues and contributors. However, the final decision to accept a merge +request is up to one of our merge request "endbosses", denoted on the +[team page](https://about.gitlab.com/team). + +## Everyone + +- Accept that many programming decisions are opinions. Discuss tradeoffs, which + you prefer, and reach a resolution quickly. +- Ask questions; don't make demands. ("What do you think about naming this + `:user_id`?") +- Ask for clarification. ("I didn't understand. Can you clarify?") +- Avoid selective ownership of code. ("mine", "not mine", "yours") +- Avoid using terms that could be seen as referring to personal traits. ("dumb", + "stupid"). Assume everyone is attractive, intelligent, and well-meaning. +- Be explicit. Remember people don't always understand your intentions online. +- Be humble. ("I'm not sure - let's look it up.") +- Don't use hyperbole. ("always", "never", "endlessly", "nothing") +- Be careful about the use of sarcasm. Everything we do is public; what seems + like good-natured ribbing to you and a long-time colleague might come off as + mean and unwelcoming to a person new to the project. +- Consider one-on-one chats or video calls if there are too many "I didn't + understand" or "Alternative solution:" comments. Post a follow-up comment + summarizing one-on-one discussion. + +## Having your code reviewed + +- The first reviewer of your code is _you_. Before you perform that first push + of your shiny new branch, read through the entire diff. Does it make sense? + Did you include something unrelated to the overall purpose of the changes? Did + you forget to remove any debugging code? +- Be grateful for the reviewer's suggestions. ("Good call. I'll make that + change.") +- Don't take it personally. The review is of the code, not of you. +- Explain why the code exists. ("It's like that because of these reasons. Would + it be more clear if I rename this class/file/method/variable?") +- Extract unrelated changes and refactorings into future merge requests/issues. +- Seek to understand the reviewer's perspective. +- Try to respond to every comment. +- Push commits based on earlier rounds of feedback as isolated commits to the + branch. Do not squash until the branch is ready to merge. Reviewers should be + able to read individual updates based on their earlier feedback. + +## Reviewing code + +Understand why the change is necessary (fixes a bug, improves the user +experience, refactors the existing code). Then: + +- Communicate which ideas you feel strongly about and those you don't. +- Identify ways to simplify the code while still solving the problem. +- Offer alternative implementations, but assume the author already considered + them. ("What do you think about using a custom validator here?") +- Seek to understand the author's perspective. +- If you don't understand a piece of code, _say so_. There's a good chance + someone else would be confused by it as well. +- After a round of line notes, it can be helpful to post a summary note such as + "LGTM :thumbsup:", or "Just a couple things to address." +- Avoid accepting a merge request before the build succeeds ("Merge when build + succeeds" is fine). + +## Credits + +Largely based on the [thoughtbot code review guide]. + +[thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index c0192bd670..c1cf2e77c2 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -2,36 +2,35 @@ GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby code. This can be used to measure the time spent in a specific part of a larger -chunk of code. The resulting data is written to a separate series. +chunk of code. The resulting data is stored as a field in the transaction that +executed the block. -To start measuring a block of Ruby code you should use -`Gitlab::Metrics.measure` and give it a name for the series to store the data -in: +To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure` +and give it a name: ```ruby -Gitlab::Metrics.measure(:user_logins) do +Gitlab::Metrics.measure(:foo) do ... end ``` -The first argument of this method is the series name and should be plural. This -name will be prefixed with `rails_` or `sidekiq_` depending on whether the code -was run in the Rails application or one of the Sidekiq workers. In the -above example the final series names would be as follows: +3 values are measured for a block: -- rails_user_logins -- sidekiq_user_logins +1. The real time elapsed, stored in NAME_real_time. +2. The CPU time elapsed, stored in NAME_cpu_time. +3. The call count, stored in NAME_call_count. -Series names should be plural as this keeps the naming style in line with the -other series names. +Both the real and CPU timings are measured in milliseconds. -By default metrics measured using a block contain a single value, "duration", -which contains the number of milliseconds it took to execute the block. Custom -values can be added by passing a Hash as the 2nd argument. Custom tags can be -added by passing a Hash as the 3rd argument. A simple example is as follows: +Multiple calls to the same block will result in the final values being the sum +of all individual values. Take this code for example: ```ruby -Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do - ... +3.times do + Gitlab::Metrics.measure(:sleep) do + sleep 1 + end end ``` + +Here the final value of `sleep_real_time` will be `3`, _not_ `1`. diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 9f3fd69fc4..6d04b9590e 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -9,7 +9,7 @@ bundle exec rake setup ``` The `setup` task is a alias for `gitlab:setup`. -This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. +This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. Note: `db:setup` calls `db:seed` but this does nothing. ## Run tests diff --git a/doc/development/testing.md b/doc/development/testing.md new file mode 100644 index 0000000000..672e3fb464 --- /dev/null +++ b/doc/development/testing.md @@ -0,0 +1,136 @@ +# Testing Standards and Style Guidelines + +This guide outlines standards and best practices for automated testing of GitLab +CE and EE. + +It is meant to be an _extension_ of the [thoughtbot testing +styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If +this guide defines a rule that contradicts the thoughtbot guide, this guide +takes precedence. Some guidelines may be repeated verbatim to stress their +importance. + +## Factories + +GitLab uses [factory_girl] as a test fixture replacement. + +- Factory definitions live in `spec/factories/`, named using the pluralization + of their corresponding model (`User` factories are defined in `users.rb`). +- There should be only one top-level factory definition per file. +- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and + should) call `create(...)` instead of `FactoryGirl.create(...)`. +- Make use of [traits] to clean up definitions and usages. +- When defining a factory, don't define attributes that are not required for the + resulting record to pass validation. +- When instantiating from a factory, don't supply attributes that aren't + required by the test. +- Factories don't have to be limited to `ActiveRecord` objects. + [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). + +[factory_girl]: https://github.com/thoughtbot/factory_girl +[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits + +## JavaScript + +GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on +the command line via `bundle exec teaspoon`, or via a web browser at +`http://localhost:3000/teaspoon` when the Rails server is running. + +- JavaScript tests live in `spec/javascripts/`, matching the folder structure of + `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding + `spec/javascripts/behaviors/autosize_spec.js.coffee` file. +- Haml fixtures required for JavaScript tests live in + `spec/javascripts/fixtures`. They should contain the bare minimum amount of + markup necessary for the test. + + > **Warning:** Keep in mind that a Rails view may change and + invalidate your test, but everything will still pass because your fixture + doesn't reflect the latest view. + +- Keep in mind that in a CI environment, these tests are run in a headless + browser and you will not have access to certain APIs, such as + [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), + which will have to be stubbed. + +[Teaspoon]: https://github.com/modeset/teaspoon +[Jasmine]: https://github.com/jasmine/jasmine + +## RSpec + +### General Guidelines + +- Use a single, top-level `describe ClassName` block. +- Use `described_class` instead of repeating the class name being described. +- Use `.method` to describe class methods and `#method` to describe instance + methods. +- Use `context` to test branching logic. +- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). +- Prefer `not_to` to `to_not`. +- Try to match the ordering of tests to the ordering within the class. +- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines + to separate phases. + +[four-phase-test]: https://robots.thoughtbot.com/four-phase-test + +### `let` variables + +GitLab's RSpec suite has made extensive use of `let` variables to reduce +duplication. However, this sometimes [comes at the cost of clarity][lets-not], +so we need to set some guidelines for their use going forward: + +- `let` variables are preferable to instance variables. Local variables are + preferable to `let` variables. +- Use `let` to reduce duplication throughout an entire spec file. +- Don't use `let` to define variables used by a single test; define them as + local variables inside the test's `it` block. +- Don't define a `let` variable inside the top-level `describe` block that's + only used in a more deeply-nested `context` or `describe` block. Keep the + definition as close as possible to where it's used. +- Try to avoid overriding the definition of one `let` variable with another. +- Don't define a `let` variable that's only used by the definition of another. + Use a helper method instead. + +[lets-not]: https://robots.thoughtbot.com/lets-not + +### Test speed + +GitLab has a massive test suite that, without parallelization, can take more +than an hour to run. It's important that we make an effort to write tests that +are accurate and effective _as well as_ fast. + +Here are some things to keep in mind regarding test performance: + +- `double` and `spy` are faster than `FactoryGirl.build(...)` +- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. +- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, + `spy`, or `double` will do. Database persistence is slow! +- Use `create(:empty_project)` instead of `create(:project)` when you don't need + the underlying Git repository. Filesystem operations are slow! +- Don't mark a feature as requiring JavaScript (through `@javascript` in + Spinach or `js: true` in RSpec) unless it's _actually_ required for the test + to be valid. Headless browser testing is slow! + +### Features / Integration + +- Feature specs live in `spec/features/` and should be named + `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. +- Use only one `feature` block per feature spec file. +- Use scenario titles that describe the success and failure paths. +- Avoid scenario titles that add no information, such as "successfully." +- Avoid scenario titles that repeat the feature title. + +## Spinach (feature) tests + +GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) +for its feature/integration tests in September 2012. + +As of March 2016, we are [trying to avoid adding new Spinach +tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, +opting for [RSpec feature](#features-integration) specs. + +Adding new Spinach scenarios is acceptable _only if_ the new scenario requires +no more than one new `step` definition. If more than that is required, the +test should be re-implemented using RSpec instead. + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 2f01defc11..a3e260a5f8 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -1,9 +1,5 @@ # UI Guide for building GitLab -## Best practices for creating new pages in GitLab - -TODO: write some best practices when develop GitLab features. - ## GitLab UI development kit We created a page inside GitLab where you can check commonly used html and css elements. diff --git a/doc/install/installation.md b/doc/install/installation.md index e0a16df09c..e721e70a59 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -283,9 +283,13 @@ sudo usermod -aG redis git # Copy the example Rack attack config sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user, used when editing via web editor + # Configure Git global settings for git user + # 'autocrlf' is needed for the web editor sudo -u git -H git config --global core.autocrlf input + # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed + sudo -u git -H git config --global gc.auto 0 + # Configure Redis connection settings sudo -u git -H cp config/resque.yml.example config/resque.yml @@ -526,6 +530,16 @@ See the [omniauth integration document](../integration/omniauth.md) GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it +### Adding your Trusted Proxies + +If you are using a reverse proxy on an separate machine, you may want to add the +proxy to the trusted proxies list. Otherwise users will appear signed in from the +proxy's IP address. + +You can add trusted proxies in `config/gitlab.yml` by customizing the `trusted_proxies` +option in section 1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) +for the changes to take effect. + ### Custom Redis Connection If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. diff --git a/doc/integration/README.md b/doc/integration/README.md index 7c8f785a61..6fe04aa2a0 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,26 +19,15 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. +[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html + + ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. -You can find these within GitLab in the Services page under Project Settings if -you are at least a master on the project. -Project Services are a bit like plugins in that they allow a lot of freedom in -adding functionality to GitLab. For example there is also a service that can -send an email every time someone pushes new commits. -Because GitLab is open source we can ship with the code and tests for all -plugins. This allows the community to keep the plugins up to date so that they -always work in newer GitLab versions. - -For an overview of what projects services are available without logging in, -please see the [project_services directory][projects-code]. - -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [Project Service]: ../project_services/project_services.md -[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services ## SSL certificate errors diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 25f3598830..cab329c0de 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -120,6 +120,29 @@ OmniAuth provider for an existing user. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. +## Configure OmniAuth Providers as External + +>**Note:** +This setting was introduced with version 8.7 of GitLab + +You can define which OmniAuth providers you want to be `external` so that all users +creating accounts via these providers will not be able to have access to internal +projects. You will need to use the full name of the provider, like `google_oauth2` +for Google. Refer to the examples for the full names of the supported providers. + +**For Omnibus installations** + +```ruby + gitlab_rails['omniauth_external_providers'] = ['twitter', 'google_oauth2'] +``` + +**For installations from source** + +```yaml + omniauth: + external_providers: ['twitter', 'google_oauth2'] +``` + ## Using Custom Omniauth Providers >**Note:** diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index a0be3dd4e5..b6b2d4e5e8 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -76,3 +76,50 @@ sudo gitlab-ctl reconfigure ``` On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in. + +## Apache 2.4 / GitLab 8.6 update +The order of the first 2 Location directives is important. If they are reversed, +you will not get a shibboleth session! + +``` + + Require all granted + ProxyPassReverse http://127.0.0.1:8181 + ProxyPassReverse http://YOUR_SERVER_FQDN/ + + + + AuthType shibboleth + ShibRequestSetting requireSession 1 + ShibUseHeaders On + Require shib-session + + + Alias /shibboleth-sp /usr/share/shibboleth + + + Require all granted + + + + SetHandler shib + + + RewriteEngine on + + #Don't escape encoded characters in api requests + RewriteCond %{REQUEST_URI} ^/api/v3/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE] + + #Forward all requests to gitlab-workhorse except existing files + RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f [OR] + RewriteCond %{REQUEST_URI} ^/uploads/.* + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA] + + RequestHeader set X_FORWARDED_PROTO 'https' + RequestHeader set X-Forwarded-Ssl on +``` \ No newline at end of file diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index b856e7935a..90e9930221 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -37,3 +37,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md index 416c9870aa..a79c8d48d3 100644 --- a/doc/monitoring/performance/grafana_configuration.md +++ b/doc/monitoring/performance/grafana_configuration.md @@ -61,24 +61,32 @@ contents below and paste it in to the interactive session: ``` CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1 -CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END -CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END -CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END +CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM rails_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM sidekiq_method_calls GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM rails_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM sidekiq_method_calls GROUP BY time(1m), method END; +CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM rails_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM sidekiq_memory_usage GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM sidekiq_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM rails_file_descriptors GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM rails_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM sidekiq_gc_statistics GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_allowed_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_allowed_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_allowed_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/allowed' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_counts_per_minute ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_internal_authorized_keys_request_counts_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_request_timings_per_minute ON gitlab BEGIN SELECT percentile("duration", 95) AS duration_95th, percentile("duration", 99) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_request_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_internal_authorized_keys_sql_timings_per_minute ON gitlab BEGIN SELECT percentile(sql_duration, 95) AS duration_95th, percentile(sql_duration, 99) AS duration_99th, mean(sql_duration) AS duration_mean INTO gitlab.seven_days.grape_internal_authorized_keys_sql_timings_per_minute FROM rails_transactions WHERE request_uri = '/api/v3/internal/authorized_keys' GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.rails_transaction_timings FROM rails_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(view_duration, 95.000) AS view_duration_95th, percentile(view_duration, 99.000) AS view_duration_99th, mean(view_duration) AS view_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.sidekiq_transaction_timings FROM sidekiq_transactions GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS count INTO gitlab.seven_days.grape_transaction_counts FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; +CREATE CONTINUOUS QUERY grape_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean, percentile(sql_duration, 95.000) AS sql_duration_95th, percentile(sql_duration, 99.000) AS sql_duration_99th, mean(sql_duration) AS sql_duration_mean, percentile(cache_read_duration, 99) AS cache_read_duration_99th, percentile(cache_read_duration, 95) AS cache_read_duration_95th, mean(cache_read_duration) AS cache_read_duration_mean, percentile(cache_write_duration, 99) AS cache_write_duration_99th, percentile(cache_write_duration, 95) AS cache_write_duration_95th, mean(cache_write_duration) AS cache_write_duration_mean, percentile(cache_delete_duration, 99) AS cache_delete_duration_99th, percentile(cache_delete_duration, 95) AS cache_delete_duration_95th, mean(cache_delete_duration) AS cache_delete_duration_mean, percentile(cache_exists_duration, 99) AS cache_exists_duration_99th, percentile(cache_exists_duration, 95) AS cache_exists_duration_95th, mean(cache_exists_duration) AS cache_exists_duration_mean, percentile(cache_duration, 99) AS cache_duration_99th, percentile(cache_duration, 95) AS cache_duration_95th, mean(cache_duration) AS cache_duration_mean INTO gitlab.seven_days.grape_transaction_timings FROM rails_transactions WHERE action !~ /.+/ GROUP BY time(1m) END; ``` ## Import Dashboards @@ -91,21 +99,25 @@ JSON file. Open the dashboard dropdown menu and click 'Import' -![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png) +![Grafana dashboard dropdown](img/grafana_dashboard_dropdown.png) Click 'Choose file' and browse to the location where you downloaded or cloned the dashboard repository. Pick one of the JSON files to import. -![Grafana dashboard import](/img/grafana_dashboard_import.png) +![Grafana dashboard import](img/grafana_dashboard_import.png) Once the dashboard is imported, be sure to click save icon in the top bar. If you do not save the dashboard after importing it will be removed when you navigate away. -![Grafana save icon](/img/grafana_save_icon.png) +![Grafana save icon](img/grafana_save_icon.png) Repeat this process for each dashboard you wish to import. +Alternatively you can automatically import all the dashboards into your Grafana +instance. See the README of the [Grafana dashboards][grafana-dashboards] +repository for more information on this process. + [grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards --- diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 3a2b598b78..63aa03985e 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -181,6 +181,7 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md [influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management [influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index a5a8aebd2d..d31b3788f3 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -85,3 +85,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) +- [Grafana Install/Configuration](grafana_configuration.md diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png index 2b37eda352..c225daa81e 100644 Binary files a/doc/project_services/img/jira_service_page.png and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 27170c1eb1..b626c746c7 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,9 +1,9 @@ # GitLab JIRA integration -_**Note:** +>**Note:** Full JIRA integration was previously exclusive to GitLab Enterprise Edition. With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well._ +to GitLab Community Edition as well. --- @@ -88,8 +88,9 @@ password as they will be needed when configuring GitLab in the next section. ### Configuring GitLab -_**Note:** The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required._ +>**Note:** +The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required. --- @@ -113,13 +114,24 @@ Fill in the required details on the page, as described in the table below. | `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | | `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot](img/jira_issues_workflow.png)). By default, this ID is set to `2` | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | After saving the configuration, your GitLab project will be able to interact with the linked JIRA project. +For example, given the settings below: + +- the JIRA URL is `https://jira.example.com` +- the project is named `GITLAB` +- the user is named `gitlab` +- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) + +the following screenshot shows how the JIRA service settings should look like. + ![JIRA service page](img/jira_service_page.png) +[trans]: img/jira_issues_workflow.png + --- ## JIRA issues diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 3fea2cff0b..a5af620d9b 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,7 +1,24 @@ # Project Services Project services allow you to integrate GitLab with other applications. Below -is list of the currently supported ones. Click on the service links to see +is list of the currently supported ones. + +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. + +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. + +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. + +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +Click on the service links to see further configuration instructions and details. Contributions are welcome. ## Services diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md index 76eee147c7..8599133a72 100644 --- a/doc/update/8.6-to-8.7.md +++ b/doc/update/8.6-to-8.7.md @@ -86,6 +86,14 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ### 7. Update configuration files +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + #### Nginx configuration Ensure you're still up-to-date with the latest NGINX configuration changes: diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png new file mode 100644 index 0000000000..394c139e17 Binary files /dev/null and b/doc/workflow/img/new_branch_from_issue.png differ diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index 4a451d9895..1832567a34 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -66,6 +66,35 @@ the target branch. Click **Create directory** to finish. ## Create a new branch +There are multiple ways to create a branch from GitLab's web interface. + +### Create a new branch from an issue + +>**Note:** +This feature was [introduced][ce-2808] in GitLab 8.6. + +In case your development workflow dictates to have an issue for every merge +request, you can quickly create a branch right on the issue page which will be +tied with the issue itself. You can see a **New Branch** button after the issue +description, unless there is already a branch with the same name or a referenced +merge request. + +![New Branch Button](img/new_branch_from_issue.png) + +Once you click it, a new branch will be created that diverges from the default +branch of your project, by default `master`. The branch name will be based on +the title of the issue and as suffix it will have its ID. Thus, the example +screenshot above will yield a branch named +`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`. + +After the branch is created, you can edit files in the repository to fix +the issue. When a merge request is created based on the newly created branch, +the description field will automatically display the [issue closing pattern] +`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the +merge request is merged. + +### Create a new branch from a project's dashboard + If you want to make changes to several files before creating a new merge request, you can create a new branch up front. From a project's files page, choose **New branch** from the dropdown. @@ -118,3 +147,6 @@ appear that is labeled **Start a new merge request with these changes**. After you commit the changes you will be taken to a new merge request form. ![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png) + +[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 +[issue closing pattern]: ../customization/issue_closing.md diff --git a/features/groups.feature b/features/groups.feature index 49e939807b..419a5d3963 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -7,6 +7,10 @@ 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/profile/notifications.feature b/features/profile/notifications.feature index 55997d44de..ef8743932f 100644 --- a/features/profile/notifications.feature +++ b/features/profile/notifications.feature @@ -7,3 +7,9 @@ Feature: Profile Notifications Scenario: I visit notifications tab When I visit profile notifications page Then I should see global notifications settings + + @javascript + Scenario: I edit Project Notifications + Given I visit profile notifications page + When I select Mention setting from dropdown + Then I should see Notification saved message diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature index 10bd6fec80..67f1e117f7 100644 --- a/features/project/forked_merge_requests.feature +++ b/features/project/forked_merge_requests.feature @@ -4,6 +4,7 @@ Feature: Project Forked Merge Requests And I am a member of project "Shop" And I have a project forked off of "Shop" called "Forked Shop" + @javascript Scenario: I submit new unassigned merge request to a forked project Given I visit project "Forked Shop" merge requests page And I click link "New Merge Request" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 823658b4f2..ecda4ea824 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -70,6 +70,7 @@ Feature: Project Merge Requests When I click link "Reopen" Then I should see reopened merge request "Bug NS-04" + @javascript Scenario: I submit new unassigned merge request Given I click link "New Merge Request" And I submit new merge request "Wiki Feature" diff --git a/features/project/project.feature b/features/project/project.feature index aa22401c88..f1f3ed2606 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,6 +18,15 @@ 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/todos.rb b/features/steps/dashboard/todos.rb index 30b21b93ac..a6e574f12a 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -26,6 +26,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should see todos assigned to me' do + page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' } expect(page).to have_content 'To do 4' expect(page).to have_content 'Done 0' @@ -41,6 +42,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end + page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index b6ce5bc9ce..a167d25983 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - page.within '.nav-secondary' do - click_link("Milestones") - end + click_link 'Milestones' 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 483370f41c..e5b7db4c5e 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -4,6 +4,10 @@ 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/profile/notifications.rb b/features/steps/profile/notifications.rb index 447ea6d9d1..a96f35ada5 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -9,4 +9,14 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps step 'I should see global notifications settings' do expect(page).to have_content "Notifications" end + + step 'I select Mention setting from dropdown' do + select 'mention', from: 'notification_setting_level' + end + + step 'I should see Notification saved message' do + page.within '.flash-container' do + expect(page).to have_content 'Notification settings saved' + end + end end diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 4584fc4d75..19d81453d8 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -82,9 +82,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - page.within '.nav-secondary' do - click_link('Milestones') - end + click_link('Milestones') end step 'I click the "Labels" tab' do diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index d9b16afa9b..527f7853da 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 '.nav-secondary' do + page.within '.page-sidebar-expanded' do click_link "Merge Requests" end end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 7e4425ff66..612bb8fd8b 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -34,10 +34,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I fill out a "Merge Request On Forked Project" merge request' do - select @forked_project.path_with_namespace, from: "merge_request_source_project_id" - select @project.path_with_namespace, from: "merge_request_target_project_id" - select "fix", from: "merge_request_source_branch" - select "master", from: "merge_request_target_branch" + first('.js-source-project').click + first('.dropdown-source-project a', text: @forked_project.path_with_namespace) + + first('.js-target-project').click + first('.dropdown-target-project a', text: @project.path_with_namespace) + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'fix').click click_button "Compare branches and continue" @@ -115,10 +119,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s - expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s - expect(find(:select, "merge_request_source_branch", {}).value).to eq "" - expect(find(:select, "merge_request_target_branch", {}).value).to eq "master" + expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s + expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s + expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil + expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master" click_button "Compare branches" end @@ -127,7 +131,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'the target repository should be the original repository' do - expect(page).to have_select("merge_request_target_project_id", selected: @project.path_with_namespace) + expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}" end step 'I click "Assign to" dropdown"' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 2ab8956867..0ca2d6257c 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -15,7 +15,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I delete all labels' do page.within '.labels' do - page.all('.btn-remove').each do |remove| + page.all('.remove-row').each do |remove| remove.click sleep 0.05 end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index a4f02b590e..f0af0d097f 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -93,8 +93,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I submit new merge request "Wiki Feature"' do - select "fix", from: "merge_request_source_branch" - select "feature", from: "merge_request_target_branch" + find('.js-source-branch').click + find('.dropdown-source-branch .dropdown-content a', text: 'fix').click + + find('.js-target-branch').click + first('.dropdown-target-branch .dropdown-content a', text: 'feature').click + click_button "Compare branches" fill_in "merge_request_title", with: "Wiki Feature" click_button "Submit merge request" diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 8f1d4a223a..ef185861e0 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,9 +114,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - page.within '.nav-secondary' do - expect(page).not_to have_link 'Snippets' - end + expect(page).not_to have_link 'Snippets' end step 'project "Shop" belongs to group' do @@ -125,6 +123,14 @@ 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/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 243469b8e7..e072505e5d 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -213,13 +213,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I see Browse file link' do - expect(page).to have_link 'Browse File »' - expect(page).not_to have_link 'Browse Files »' + expect(page).to have_link 'Browse File' + expect(page).not_to have_link 'Browse Files' end step 'I see Browse code link' do - expect(page).to have_link 'Browse Files »' - expect(page).not_to have_link 'Browse File »' + expect(page).to have_link 'Browse Files' expect(page).not_to have_link 'Browse Directory »' end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 32c3e99f45..1448c3f44c 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -155,7 +155,7 @@ module SharedDiffNote step 'I should see a discussion reply button' do page.within(diff_file_selector) do - expect(page).to have_button('Reply') + expect(page).to have_button('Reply...') end end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index b6d70a26c2..24b3fb6eac 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -71,13 +71,16 @@ module SharedIssuable step 'I should not see any related merge requests' do page.within '.issue-details' do - expect(page).not_to have_content('.merge-requests') + expect(page).not_to have_content('#merge-requests .merge-requests-title') end end step 'I should see the "Enterprise fix" related merge request' do - page.within '.merge-requests' do + page.within '#merge-requests .merge-requests-title' do expect(page).to have_content('1 Related Merge Request') + end + + page.within '#merge-requests ul' do expect(page).to have_content('Enterprise fix') end end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index fa7d24ce61..4fc2ece79f 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-secondary' do + page.within '.nav-sidebar' do expect(page).to have_content('Go to project') end end diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 18d6e93e0f..41ca617847 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -64,21 +64,41 @@ "unicode": "1F6EA", "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" }, + { + "name": "northeast_pointing_airplane", + "unicode": "1F6EA", + "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4" + }, { "name": "airplane_small", "unicode": "1F6E9", "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" }, + { + "name": "small_airplane", + "unicode": "1F6E9", + "digest": "f98b44422d6bf505b50330805ecf68013d035341f0b6487c3c05ad913eb5abd3" + }, { "name": "airplane_small_up", "unicode": "1F6E8", "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" }, + { + "name": "up_pointing_small_airplane", + "unicode": "1F6E8", + "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a" + }, { "name": "airplane_up", "unicode": "1F6E7", "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" }, + { + "name": "up_pointing_airplane", + "unicode": "1F6E7", + "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77" + }, { "name": "alarm_clock", "unicode": "23F0", @@ -149,11 +169,21 @@ "unicode": "1F5EE", "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" }, + { + "name": "left_anger_bubble", + "unicode": "1F5EE", + "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc" + }, { "name": "anger_right", "unicode": "1F5EF", "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" }, + { + "name": "right_anger_bubble", + "unicode": "1F5EF", + "digest": "24b572d64c519251a3ae8844e8d66fd6955752aff99aebe7dc20179505a466c4" + }, { "name": "angry", "unicode": "1F620", @@ -304,6 +334,11 @@ "unicode": "002A-20E3", "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" }, + { + "name": "keycap_asterisk", + "unicode": "002A-20E3", + "digest": "0b7f27f545b616677c83d40ff957337477b2881459b4d3c839ae55e23797419f" + }, { "name": "astonished", "unicode": "1F632", @@ -324,6 +359,11 @@ "unicode": "269B", "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" }, + { + "name": "atom_symbol", + "unicode": "269B", + "digest": "cbce1725602efbb77a935cfae5407e4d75489ee988910296c7f6140665afc669" + }, { "name": "b", "unicode": "1F171", @@ -399,11 +439,21 @@ "unicode": "1F5F3", "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" }, + { + "name": "ballot_box_with_ballot", + "unicode": "1F5F3", + "digest": "0455ea75612efe78354315b4c345953d2d559bb471d5b01c1adc1d6b74ed693a" + }, { "name": "ballot_box_check", "unicode": "1F5F9", "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" }, + { + "name": "ballot_box_with_bold_check", + "unicode": "1F5F9", + "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15" + }, { "name": "ballot_box_with_check", "unicode": "2611", @@ -414,11 +464,21 @@ "unicode": "1F5F5", "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" }, + { + "name": "ballot_box_with_script_x", + "unicode": "1F5F5", + "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928" + }, { "name": "ballot_x", "unicode": "1F5F4", "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" }, + { + "name": "ballot_script_x", + "unicode": "1F5F4", + "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1" + }, { "name": "bamboo", "unicode": "1F38D", @@ -464,31 +524,61 @@ "unicode": "26F9", "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" }, + { + "name": "person_with_ball", + "unicode": "26F9", + "digest": "e94beb69f631667479a80095bf313ceb3aa109d6ebb80f182722360a6d2a214e" + }, { "name": "basketball_player_tone1", "unicode": "26F9-1F3FB", "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" }, + { + "name": "person_with_ball_tone1", + "unicode": "26F9-1F3FB", + "digest": "6fc77cf2f26ee18e9a3faea500d4277839f77633f31ee618a68c301f1ad32d90" + }, { "name": "basketball_player_tone2", "unicode": "26F9-1F3FC", "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" }, + { + "name": "person_with_ball_tone2", + "unicode": "26F9-1F3FC", + "digest": "6ee9060c24d92708e12a854fb0bdf5c717c90b8c0350d8aa40c278b41bfa12fc" + }, { "name": "basketball_player_tone3", "unicode": "26F9-1F3FD", "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" }, + { + "name": "person_with_ball_tone3", + "unicode": "26F9-1F3FD", + "digest": "752e90dbfa7c7a9ae3f37de924e22f3c3d5a7e54dd41c8e8eb99cabb0dad73cf" + }, { "name": "basketball_player_tone4", "unicode": "26F9-1F3FE", "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" }, + { + "name": "person_with_ball_tone4", + "unicode": "26F9-1F3FE", + "digest": "38bedc3074e6243454d568d9b665f5764f1a3d983875651ce7a1cdb53da9f6c8" + }, { "name": "basketball_player_tone5", "unicode": "26F9-1F3FF", "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" }, + { + "name": "person_with_ball_tone5", + "unicode": "26F9-1F3FF", + "digest": "25ee1e84670d3db96d3ad098c859abd6b3448f55f668ce0c195ee2337a215de7" + }, { "name": "bath", "unicode": "1F6C0", @@ -534,11 +624,21 @@ "unicode": "1F3D6", "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" }, + { + "name": "beach_with_umbrella", + "unicode": "1F3D6", + "digest": "52855d75cfa4476ccc23c58b4afcb76ee48abb22a9a6081210c8accefdf33099" + }, { "name": "beach_umbrella", "unicode": "26F1", "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" }, + { + "name": "umbrella_on_ground", + "unicode": "26F1", + "digest": "cefe8e195d21d3e0769d3bfe15170db9e57c86db9d31cacb19fcdc8d2191b661" + }, { "name": "bear", "unicode": "1F43B", @@ -584,6 +684,11 @@ "unicode": "1F6CE", "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" }, + { + "name": "bellhop_bell", + "unicode": "1F6CE", + "digest": "c15455f1b52ac26404b5c13a0e1070212ed1830026422873f4f6335e26e31259" + }, { "name": "bento", "unicode": "1F371", @@ -634,6 +739,11 @@ "unicode": "2623", "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" }, + { + "name": "biohazard_sign", + "unicode": "2623", + "digest": "81f8309318051255ed4dc18855a3cd3f8657a6f3b2d368caa531a57ce0e34235" + }, { "name": "bird", "unicode": "1F426", @@ -769,6 +879,11 @@ "unicode": "1F395", "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" }, + { + "name": "bouquet_of_flowers", + "unicode": "1F395", + "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d" + }, { "name": "bow", "unicode": "1F647", @@ -779,6 +894,11 @@ "unicode": "1F3F9", "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" }, + { + "name": "archery", + "unicode": "1F3F9", + "digest": "1c23469256331ea4ff03c036f89f0e63ad3228c51faecba50129da99b7eaddf3" + }, { "name": "bow_tone1", "unicode": "1F647-1F3FB", @@ -924,6 +1044,11 @@ "unicode": "1F56C", "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" }, + { + "name": "bullhorn_with_sound_waves", + "unicode": "1F56C", + "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c" + }, { "name": "burrito", "unicode": "1F32F", @@ -964,6 +1089,11 @@ "unicode": "1F5A9", "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" }, + { + "name": "pocket calculator", + "unicode": "1F5A9", + "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c" + }, { "name": "calendar", "unicode": "1F4C6", @@ -974,6 +1104,11 @@ "unicode": "1F5D3", "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" }, + { + "name": "spiral_calendar_pad", + "unicode": "1F5D3", + "digest": "1dd5da98bb435c0c3f632bc0a5c9fdde694de7aee752bf4bb85def086e788a2a" + }, { "name": "calling", "unicode": "1F4F2", @@ -1034,6 +1169,11 @@ "unicode": "1F5C3", "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" }, + { + "name": "card_file_box", + "unicode": "1F5C3", + "digest": "7d760ae1d44e6f4b2aac00895ca86b5743f8b5ca157ec2bd21ce2665e50ad23a" + }, { "name": "card_index", "unicode": "1F4C7", @@ -1049,6 +1189,11 @@ "unicode": "1F5AD", "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" }, + { + "name": "tape_cartridge", + "unicode": "1F5AD", + "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2" + }, { "name": "cat", "unicode": "1F431", @@ -1079,6 +1224,11 @@ "unicode": "1F37E", "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" }, + { + "name": "bottle_with_popping_cork", + "unicode": "1F37E", + "digest": "77395d3afe5cc10bfdc381120bae2ae4aefdaa96c529536413873a696c5fa713" + }, { "name": "chart", "unicode": "1F4B9", @@ -1104,6 +1254,11 @@ "unicode": "1F9C0", "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" }, + { + "name": "cheese_wedge", + "unicode": "1F9C0", + "digest": "5897036ba97b557868bb314fcee83b9d8a609c8447b270a0b3d34a29ce7496d1" + }, { "name": "cherries", "unicode": "1F352", @@ -1169,6 +1324,11 @@ "unicode": "1F307", "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" }, + { + "name": "city_sunrise", + "unicode": "1F307", + "digest": "c2530d12204eb518c5a3c8d7deba11170b1412fdf406aea05a69d4c026210d1b" + }, { "name": "cityscape", "unicode": "1F3D9", @@ -1229,6 +1389,11 @@ "unicode": "1F570", "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" }, + { + "name": "mantlepiece_clock", + "unicode": "1F570", + "digest": "c48314ccde8bf01acc2b1bc9a6b5aa7d796fc0c8769f80398bc74545fcef31ed" + }, { "name": "clock1", "unicode": "1F550", @@ -1354,6 +1519,11 @@ "unicode": "1F5D8", "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" }, + { + "name": "clockwise_right_and_left_semicircle_arrows", + "unicode": "1F5D8", + "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622" + }, { "name": "closed_book", "unicode": "1F4D5", @@ -1379,21 +1549,41 @@ "unicode": "1F329", "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" }, + { + "name": "cloud_with_lightning", + "unicode": "1F329", + "digest": "fc9c85cc95f9c456635692c974f72b6d40e14943824b8129a21c47265c3416f4" + }, { "name": "cloud_rain", "unicode": "1F327", "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" }, + { + "name": "cloud_with_rain", + "unicode": "1F327", + "digest": "f4406e62ed98f6141ab70736f6d5c540023e805396db0346ee6b7082c3f5e8e2" + }, { "name": "cloud_snow", "unicode": "1F328", "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" }, + { + "name": "cloud_with_snow", + "unicode": "1F328", + "digest": "948990cd13dd927917208c026089519fcf8e258a8a284684ace67c9a2f9a8149" + }, { "name": "cloud_tornado", "unicode": "1F32A", "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" }, + { + "name": "cloud_with_tornado", + "unicode": "1F32A", + "digest": "44753516d0bd05d47cfa6eb922aba570ba6a87f805f325772b2cff071460ead1" + }, { "name": "clubs", "unicode": "2663", @@ -1439,6 +1629,11 @@ "unicode": "1F5B3", "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" }, + { + "name": "old_personal_computer", + "unicode": "1F5B3", + "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3" + }, { "name": "confetti_ball", "unicode": "1F38A", @@ -1509,6 +1704,11 @@ "unicode": "1F3D7", "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" }, + { + "name": "building_construction", + "unicode": "1F3D7", + "digest": "0ff52e6adf1927d356b27be5fef6bad2ad842be05e3a0bd16a17efe78e5676d9" + }, { "name": "convenience_store", "unicode": "1F3EA", @@ -1569,6 +1769,11 @@ "unicode": "1F6CB", "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" }, + { + "name": "couch_and_lamp", + "unicode": "1F6CB", + "digest": "a93fffed194b404200495abda8772bb35539cfc8499eb0a9bf09c508afad6676" + }, { "name": "couple", "unicode": "1F46B", @@ -1579,6 +1784,11 @@ "unicode": "1F468-2764-1F468", "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" }, + { + "name": "couple_with_heart_mm", + "unicode": "1F468-2764-1F468", + "digest": "3ae6fbf3ba168256ea85c756ac1e7b83fdb8b780d33f06128ed80706ff627eea" + }, { "name": "couple_with_heart", "unicode": "1F491", @@ -1589,6 +1799,11 @@ "unicode": "1F469-2764-1F469", "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" }, + { + "name": "couple_with_heart_ww", + "unicode": "1F469-2764-1F469", + "digest": "d2a2ec29c1a1234ea0aa1d9fc6707cf8be8bb36ea8b92523ffa1c3071bcf0b06" + }, { "name": "couplekiss", "unicode": "1F48F", @@ -1614,6 +1829,11 @@ "unicode": "1F58D", "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" }, + { + "name": "lower_left_crayon", + "unicode": "1F58D", + "digest": "0f3351c2e68a8d47d27b45a9901be6160de0f9a291bd8680df84d0fc679bcb31" + }, { "name": "credit_card", "unicode": "1F4B3", @@ -1629,6 +1849,11 @@ "unicode": "1F3CF", "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" }, + { + "name": "cricket_bat_ball", + "unicode": "1F3CF", + "digest": "00eb11254e887c71db5e8945ad211e9e0280f1e02f4b77a4799b64bba2bbe9b3" + }, { "name": "crocodile", "unicode": "1F40A", @@ -1639,21 +1864,41 @@ "unicode": "271D", "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" }, + { + "name": "latin_cross", + "unicode": "271D", + "digest": "a6e3c345cf6aa2ce690b66454066b53ef5b1dab2ed635e21f1586b1dffc5df42" + }, { "name": "cross_heavy", "unicode": "1F547", "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" }, + { + "name": "heavy_latin_cross", + "unicode": "1F547", + "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70" + }, { "name": "cross_white", "unicode": "1F546", "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" }, + { + "name": "white_latin_cross", + "unicode": "1F546", + "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6" + }, { "name": "crossbones", "unicode": "1F571", "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" }, + { + "name": "black_skull_and_crossbones", + "unicode": "1F571", + "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584" + }, { "name": "crossed_flags", "unicode": "1F38C", @@ -1674,6 +1919,11 @@ "unicode": "1F6F3", "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" }, + { + "name": "passenger_ship", + "unicode": "1F6F3", + "digest": "90519c46ddfb63e71bc76661953da9041e5f0b97e9f8a7a8696518b4d529f3dd" + }, { "name": "cry", "unicode": "1F622", @@ -1729,6 +1979,11 @@ "unicode": "1F5E1", "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" }, + { + "name": "dagger_knife", + "unicode": "1F5E1", + "digest": "377060a7ce930566a4732b361be98e8a193a546846dfbba2a00abeeef41d1976" + }, { "name": "dancer", "unicode": "1F483", @@ -1814,6 +2069,11 @@ "unicode": "1F5A5", "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" }, + { + "name": "desktop_computer", + "unicode": "1F5A5", + "digest": "ba46323e695918e7253f1013cb991efb09790581c74c07c38bc5e10a20b8e8de" + }, { "name": "desktop_window", "unicode": "1F5D4", @@ -1844,6 +2104,11 @@ "unicode": "1F5C2", "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" }, + { + "name": "card_index_dividers", + "unicode": "1F5C2", + "digest": "bf4c303452a4c0b4986925041dbec5b7e478060d560630b7c5bc2f997fcad668" + }, { "name": "dizzy", "unicode": "1F4AB", @@ -1869,6 +2134,11 @@ "unicode": "1F5B9", "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" }, + { + "name": "document_with_text", + "unicode": "1F5B9", + "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72" + }, { "name": "dog", "unicode": "1F436", @@ -1909,6 +2179,11 @@ "unicode": "1F54A", "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" }, + { + "name": "dove_of_peace", + "unicode": "1F54A", + "digest": "4e2e9c47e5632efe6ccf945d61dbc2f1155a2e52905e17f307b502a2c951bdb8" + }, { "name": "dragon", "unicode": "1F409", @@ -1944,6 +2219,11 @@ "unicode": "1F4E7", "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" }, + { + "name": "email", + "unicode": "1F4E7", + "digest": "12135310cfedc091d120426f5b132df82b538c5fcad458bf6b21588f353c3adb" + }, { "name": "ear", "unicode": "1F442", @@ -2044,21 +2324,41 @@ "unicode": "1F582", "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" }, + { + "name": "back_of_envelope", + "unicode": "1F582", + "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75" + }, { "name": "envelope_flying", "unicode": "1F585", "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" }, + { + "name": "flying_envelope", + "unicode": "1F585", + "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214" + }, { "name": "envelope_stamped", "unicode": "1F583", "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" }, + { + "name": "stamped_envelope", + "unicode": "1F583", + "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468" + }, { "name": "envelope_stamped_pen", "unicode": "1F586", "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" }, + { + "name": "pen_over_stamped_envelope", + "unicode": "1F586", + "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33" + }, { "name": "envelope_with_arrow", "unicode": "1F4E9", @@ -2254,31 +2554,61 @@ "unicode": "1F597", "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" }, + { + "name": "white_down_pointing_left_hand_index", + "unicode": "1F597", + "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9" + }, { "name": "finger_pointing_down2", "unicode": "1F59F", "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" }, + { + "name": "sideways_white_down_pointing_index", + "unicode": "1F59F", + "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee" + }, { "name": "finger_pointing_left", "unicode": "1F598", "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" }, + { + "name": "sideways_white_left_pointing_index", + "unicode": "1F598", + "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e" + }, { "name": "finger_pointing_right", "unicode": "1F599", "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" }, + { + "name": "sideways_white_right_pointing_index", + "unicode": "1F599", + "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d" + }, { "name": "finger_pointing_up", "unicode": "1F59E", "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" }, + { + "name": "sideways_white_up_pointing_index", + "unicode": "1F59E", + "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d" + }, { "name": "fire", "unicode": "1F525", "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" }, + { + "name": "flame", + "unicode": "1F525", + "digest": "b44311874681135acbb5e7226febe4365c732da3a9617f10d7074a3b1ade1641" + }, { "name": "fire_engine", "unicode": "1F692", @@ -2289,6 +2619,11 @@ "unicode": "1F6F1", "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" }, + { + "name": "oncoming_fire_engine", + "unicode": "1F6F1", + "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6" + }, { "name": "fireworks", "unicode": "1F386", @@ -2359,1296 +2694,2596 @@ "unicode": "1F1E6-1F1E8", "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" }, + { + "name": "ac", + "unicode": "1F1E6-1F1E8", + "digest": "d9db1edeb709824a1083c2bba79ca5f683ed0edded35918bb167d1ee7396c8da" + }, { "name": "flag_ad", "unicode": "1F1E6-1F1E9", "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" }, + { + "name": "ad", + "unicode": "1F1E6-1F1E9", + "digest": "04a8c1745d9b8b20e903302379f2557e8082f72e33878db4cb2cd6b33eb97952" + }, { "name": "flag_ae", "unicode": "1F1E6-1F1EA", "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" }, + { + "name": "ae", + "unicode": "1F1E6-1F1EA", + "digest": "868324ac2e7bea1547f5de95f39633b77b8d62f3b3433b3d1a4ee96d169a09cd" + }, { "name": "flag_af", "unicode": "1F1E6-1F1EB", "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" }, + { + "name": "af", + "unicode": "1F1E6-1F1EB", + "digest": "9a94458519e9db5d6cf1557e54fdf62d7e48aaf7de25744a093ec8f284656226" + }, { "name": "flag_ag", "unicode": "1F1E6-1F1EC", "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" }, + { + "name": "ag", + "unicode": "1F1E6-1F1EC", + "digest": "ea59fabc2bd9024df06a59a34412f52bebfeb03eb6abd73d8fe153e3a68e28f4" + }, { "name": "flag_ai", "unicode": "1F1E6-1F1EE", "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" }, + { + "name": "ai", + "unicode": "1F1E6-1F1EE", + "digest": "75676ded736ad2ebb921e9fd8ebfef49819a35c3dcf005bbc3b7e8c6e75178f2" + }, { "name": "flag_al", "unicode": "1F1E6-1F1F1", "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" }, + { + "name": "al", + "unicode": "1F1E6-1F1F1", + "digest": "77b835dcff399b609e2479cbf10f08344c8fc277370ba8e4540165ca15563847" + }, { "name": "flag_am", "unicode": "1F1E6-1F1F2", "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" }, + { + "name": "am", + "unicode": "1F1E6-1F1F2", + "digest": "3b820c628dd5a93137f7288a43553778f60b0beea4c0a239d063893c0723e73d" + }, { "name": "flag_ao", "unicode": "1F1E6-1F1F4", "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" }, + { + "name": "ao", + "unicode": "1F1E6-1F1F4", + "digest": "d26439d4ecbe8b67bb1ae9753454505358ebb6b802624f19800471e53ee27187" + }, { "name": "flag_aq", "unicode": "1F1E6-1F1F6", "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" }, + { + "name": "aq", + "unicode": "1F1E6-1F1F6", + "digest": "6b0b4e800d88ab289ae4b6d449bfa115e92543958b477d13ad348468a74e4616" + }, { "name": "flag_ar", "unicode": "1F1E6-1F1F7", "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" }, + { + "name": "ar", + "unicode": "1F1E6-1F1F7", + "digest": "ca76db601dd3f5794f1caace8ab5641fe3786b86e4ae030706162f0ce07d27b3" + }, { "name": "flag_as", "unicode": "1F1E6-1F1F8", "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" }, + { + "name": "as", + "unicode": "1F1E6-1F1F8", + "digest": "170e1dde0e3fd2e0f2149de5cc8845e15580cc0412e81a643d61bd387de16141" + }, { "name": "flag_at", "unicode": "1F1E6-1F1F9", "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" }, + { + "name": "at", + "unicode": "1F1E6-1F1F9", + "digest": "0ab3675a16b4988e87c81e87453c160d6616c7be76247f54c471dc63aa8b42ba" + }, { "name": "flag_au", "unicode": "1F1E6-1F1FA", "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" }, + { + "name": "au", + "unicode": "1F1E6-1F1FA", + "digest": "b6f17d3dfd3547c069a0b6cddd4cf44fb8ce1d1d300e24284fb292ac142537e3" + }, { "name": "flag_aw", "unicode": "1F1E6-1F1FC", "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" }, + { + "name": "aw", + "unicode": "1F1E6-1F1FC", + "digest": "7857bc907f04dfb7ccc4401c05034ad8afb6383a022db77973cfcafa4d6c16c8" + }, { "name": "flag_ax", "unicode": "1F1E6-1F1FD", "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" }, + { + "name": "ax", + "unicode": "1F1E6-1F1FD", + "digest": "ab8f1fd4af7c220a54d478cec5a9f7f3beb5fc83439c448f3ac9848af8391ac1" + }, { "name": "flag_az", "unicode": "1F1E6-1F1FF", "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" }, + { + "name": "az", + "unicode": "1F1E6-1F1FF", + "digest": "187cc7b6d39800c5910a34409db1e6b1d8aac808c72a93e922a419d9b054fd0b" + }, { "name": "flag_ba", "unicode": "1F1E7-1F1E6", "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" }, + { + "name": "ba", + "unicode": "1F1E7-1F1E6", + "digest": "cd22c744213087384cf79ed314742026787212c9ceb6999ed166534670f7864a" + }, { "name": "flag_bb", "unicode": "1F1E7-1F1E7", "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" }, + { + "name": "bb", + "unicode": "1F1E7-1F1E7", + "digest": "44ff0a48ac2d2180374baa58b1b7c64f26d0d151a48811eb08ffa20758104512" + }, { "name": "flag_bd", "unicode": "1F1E7-1F1E9", "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" }, + { + "name": "bd", + "unicode": "1F1E7-1F1E9", + "digest": "c18793d2b963458607a0bab94c57e62c8278fce870e96fd8dda78067a8fbde18" + }, { "name": "flag_be", "unicode": "1F1E7-1F1EA", "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" }, + { + "name": "be", + "unicode": "1F1E7-1F1EA", + "digest": "6e6ccfca064a43b93c8acc04a9425f95af204198022ca20b9ee6c491e99ad950" + }, { "name": "flag_bf", "unicode": "1F1E7-1F1EB", "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" }, + { + "name": "bf", + "unicode": "1F1E7-1F1EB", + "digest": "d69c0394a1c7cb6323f54f024b7d740c728f229ca5e1b54ac374d5024f5470a5" + }, { "name": "flag_bg", "unicode": "1F1E7-1F1EC", "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" }, + { + "name": "bg", + "unicode": "1F1E7-1F1EC", + "digest": "413a270caf4a9155e84bdba6c9512277f5642246f6ba8d701383a5eeb02f7e95" + }, { "name": "flag_bh", "unicode": "1F1E7-1F1ED", "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" }, + { + "name": "bh", + "unicode": "1F1E7-1F1ED", + "digest": "9243ed65d7f24c824c2a3207335a2d4ad25251258547c16d0b7b7cbb9df6f8de" + }, { "name": "flag_bi", "unicode": "1F1E7-1F1EE", "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" }, + { + "name": "bi", + "unicode": "1F1E7-1F1EE", + "digest": "63056519030524b2d2dcd47448267d817205dbd6b98075c97f011a8f1d4d1a4b" + }, { "name": "flag_bj", "unicode": "1F1E7-1F1EF", "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" }, + { + "name": "bj", + "unicode": "1F1E7-1F1EF", + "digest": "93b245eed85d22260d27d1a8c77f51fb3439309e09b2aeca6cd504dbea77b509" + }, { "name": "flag_bl", "unicode": "1F1E7-1F1F1", "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" }, + { + "name": "bl", + "unicode": "1F1E7-1F1F1", + "digest": "5e1e478deaf02bbaa26595e4cefc5f5c9bec6105ce521b7b9ab4fa5e7a452c14" + }, { "name": "flag_black", "unicode": "1F3F4", "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" }, + { + "name": "waving_black_flag", + "unicode": "1F3F4", + "digest": "df131e5c28e9f51dea53fe7f33551f91d420f7d686b7a62980f0154c6b5357a6" + }, { "name": "flag_bm", "unicode": "1F1E7-1F1F2", "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" }, + { + "name": "bm", + "unicode": "1F1E7-1F1F2", + "digest": "9dcd9e60faebe7f93eb19157e99f2ad654a8145c61738de96e6ecd11a246764a" + }, { "name": "flag_bn", "unicode": "1F1E7-1F1F3", "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" }, + { + "name": "bn", + "unicode": "1F1E7-1F1F3", + "digest": "078af6ca481a77871ba005e251a46ce63951c27b1b0cd33b9c1d0d31d349bc1a" + }, { "name": "flag_bo", "unicode": "1F1E7-1F1F4", "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" }, + { + "name": "bo", + "unicode": "1F1E7-1F1F4", + "digest": "92516d04e922a3bcbabe2e7619194bc972c09ba97576e8155f9829c397a71d8c" + }, { "name": "flag_bq", "unicode": "1F1E7-1F1F6", "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" }, + { + "name": "bq", + "unicode": "1F1E7-1F1F6", + "digest": "7832df5267a2bb8dddb83aeb11162ce79aeebdb718f2ac0e54adcf3d87936171" + }, { "name": "flag_br", "unicode": "1F1E7-1F1F7", "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" }, + { + "name": "br", + "unicode": "1F1E7-1F1F7", + "digest": "aabcc1c082124045ed214f7d9778d8e2ed791ebb8433defea91db458658abeec" + }, { "name": "flag_bs", "unicode": "1F1E7-1F1F8", "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" }, + { + "name": "bs", + "unicode": "1F1E7-1F1F8", + "digest": "f628f39003608e181696634929522884165e27ccef55270293f92eeef991635f" + }, { "name": "flag_bt", "unicode": "1F1E7-1F1F9", "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" }, + { + "name": "bt", + "unicode": "1F1E7-1F1F9", + "digest": "af24a8ab34815da04c3e5af49a47449e0de93b068957cbda695816d0f830ca12" + }, { "name": "flag_bv", "unicode": "1F1E7-1F1FB", "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" }, + { + "name": "bv", + "unicode": "1F1E7-1F1FB", + "digest": "ff0037f6eed95d4bb5f2b502902360e1ff41426e2896daf3e0730cef1f8f7e41" + }, { "name": "flag_bw", "unicode": "1F1E7-1F1FC", "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" }, + { + "name": "bw", + "unicode": "1F1E7-1F1FC", + "digest": "3e3241ecb97946cc3e467b083d113a57dd305595e1512d4da18cc403e8689c1d" + }, { "name": "flag_by", "unicode": "1F1E7-1F1FE", "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" }, + { + "name": "by", + "unicode": "1F1E7-1F1FE", + "digest": "bdd21885c6fac475241884a44149b887297772e17617ee59dd9fe8518d52cf3d" + }, { "name": "flag_bz", "unicode": "1F1E7-1F1FF", "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" }, + { + "name": "bz", + "unicode": "1F1E7-1F1FF", + "digest": "21c16e1da641af004576000bf1db44b9a1e0fccfddc775e96022721c2f18eeea" + }, { "name": "flag_ca", "unicode": "1F1E8-1F1E6", "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" }, + { + "name": "ca", + "unicode": "1F1E8-1F1E6", + "digest": "0d00e459084d58d3ea9c60488a9e51bf45f71b77f1600f190225d5ca6ca6c796" + }, { "name": "flag_cc", "unicode": "1F1E8-1F1E8", "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" }, + { + "name": "cc", + "unicode": "1F1E8-1F1E8", + "digest": "86ab27164603ef0f1f83fe898eda6fbb7bc5709f2518f5577f00817860806a7b" + }, { "name": "flag_cd", "unicode": "1F1E8-1F1E9", "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" }, + { + "name": "congo", + "unicode": "1F1E8-1F1E9", + "digest": "fdc2796530ada4bd0bae37ace4bbe707b321b287dcd64568f8e01d3a9df56066" + }, { "name": "flag_cf", "unicode": "1F1E8-1F1EB", "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" }, + { + "name": "cf", + "unicode": "1F1E8-1F1EB", + "digest": "5943bec02bede0931e21e7c34a68f375499f60a34883cc1edf2f21e9834b15ce" + }, { "name": "flag_cg", "unicode": "1F1E8-1F1EC", "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" }, + { + "name": "cg", + "unicode": "1F1E8-1F1EC", + "digest": "54498482e2772371e148e05cfb7c5eb55f6a22cd528662abdea10bad47d157da" + }, { "name": "flag_ch", "unicode": "1F1E8-1F1ED", "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" }, + { + "name": "ch", + "unicode": "1F1E8-1F1ED", + "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" + }, { "name": "flag_ci", "unicode": "1F1E8-1F1EE", "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" }, + { + "name": "ci", + "unicode": "1F1E8-1F1EE", + "digest": "3a173a3058a5c0174dc88750852cafec264e901ce82a6c69db122c8c0ea71a3a" + }, { "name": "flag_ck", "unicode": "1F1E8-1F1F0", "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" }, + { + "name": "ck", + "unicode": "1F1E8-1F1F0", + "digest": "42f395ff53c618b72b8a224cd4343d1a32f5ad82ced56bf590170a5ff0d5134c" + }, { "name": "flag_cl", "unicode": "1F1E8-1F1F1", "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" }, + { + "name": "chile", + "unicode": "1F1E8-1F1F1", + "digest": "9d6255feb690596904d800e72d5acdb5cda941c5a741b031ea39a3c7650ac46f" + }, { "name": "flag_cm", "unicode": "1F1E8-1F1F2", "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" }, + { + "name": "cm", + "unicode": "1F1E8-1F1F2", + "digest": "ffc99d14e0a8b46a980331090ed9f36f31a87f1b0f8dd8c09007a31c6127c69e" + }, { "name": "flag_cn", "unicode": "1F1E8-1F1F3", "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" }, + { + "name": "cn", + "unicode": "1F1E8-1F1F3", + "digest": "869a98c52bdc33591f87e2aab6cb4f13e98bb19136250ff25805d0312a8b7c8a" + }, { "name": "flag_co", "unicode": "1F1E8-1F1F4", "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" }, + { + "name": "co", + "unicode": "1F1E8-1F1F4", + "digest": "6aa458440eb2500ad307fea40fd8f1171a1506a6e32af144a4fd51545bb56151" + }, { "name": "flag_cp", "unicode": "1F1E8-1F1F5", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "cp", + "unicode": "1F1E8-1F1F5", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_cr", "unicode": "1F1E8-1F1F7", "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" }, + { + "name": "cr", + "unicode": "1F1E8-1F1F7", + "digest": "0f3b54d8330c5bb136647547dafc598bda755697cfd6b7d872a2443ba7b5cad4" + }, { "name": "flag_cu", "unicode": "1F1E8-1F1FA", "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" }, + { + "name": "cu", + "unicode": "1F1E8-1F1FA", + "digest": "69bc973002475bb3d9b54cb0ba9ec9cb85f144c1cf54689da0ee8f414ebb0d83" + }, { "name": "flag_cv", "unicode": "1F1E8-1F1FB", "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" }, + { + "name": "cv", + "unicode": "1F1E8-1F1FB", + "digest": "af2e135cf3c1b03a5937c068a75061b5cd332e95902fd0f8dffb2ac2dc89692a" + }, { "name": "flag_cw", "unicode": "1F1E8-1F1FC", "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" }, + { + "name": "cw", + "unicode": "1F1E8-1F1FC", + "digest": "df4b2228a82f766c5c64c13c1388482a68549e59dd843671ee0eb43506e33411" + }, { "name": "flag_cx", "unicode": "1F1E8-1F1FD", "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" }, + { + "name": "cx", + "unicode": "1F1E8-1F1FD", + "digest": "db12e513345a7be53954167d359ede0b3effbfb292508ee4d726123e3a8f83d7" + }, { "name": "flag_cy", "unicode": "1F1E8-1F1FE", "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" }, + { + "name": "cy", + "unicode": "1F1E8-1F1FE", + "digest": "0cea41d4820746e2c6eb408f7ec7419afba9f7396401d92e6c1d77382f721d0b" + }, { "name": "flag_cz", "unicode": "1F1E8-1F1FF", "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" }, + { + "name": "cz", + "unicode": "1F1E8-1F1FF", + "digest": "a1c2405916963be306f761539123486a2845af53716c9dfe94ad5420e14d36c4" + }, { "name": "flag_de", "unicode": "1F1E9-1F1EA", "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" }, + { + "name": "de", + "unicode": "1F1E9-1F1EA", + "digest": "74a80b64437bc4e31bdd7cbb753ecd2d719bf34c506cbac535db83a644174cce" + }, { "name": "flag_dg", "unicode": "1F1E9-1F1EC", "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" }, + { + "name": "dg", + "unicode": "1F1E9-1F1EC", + "digest": "13cb5ea872f94a9c3fb579cef417e2d1ed38e8cbe95059576380cacd59bc4b9d" + }, { "name": "flag_dj", "unicode": "1F1E9-1F1EF", "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" }, + { + "name": "dj", + "unicode": "1F1E9-1F1EF", + "digest": "5b479654c28d3eeb70055c5e25dc46ccaba9eeea7537cc45ca9dbb8186b743b6" + }, { "name": "flag_dk", "unicode": "1F1E9-1F1F0", "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" }, + { + "name": "dk", + "unicode": "1F1E9-1F1F0", + "digest": "dee7fa9644a9b447417518a353e7edcbb37b2af8bc7d13a6ed71d7210c43ca3c" + }, { "name": "flag_dm", "unicode": "1F1E9-1F1F2", "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" }, + { + "name": "dm", + "unicode": "1F1E9-1F1F2", + "digest": "2e339190a8a0a238140f42e329f6646af5be75763a787ea268488a2e0440dc4c" + }, { "name": "flag_do", "unicode": "1F1E9-1F1F4", "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" }, + { + "name": "do", + "unicode": "1F1E9-1F1F4", + "digest": "be5dafcd32d7197a96d37299a91835a8009299452f05a66d91c5fdec17448230" + }, { "name": "flag_dz", "unicode": "1F1E9-1F1FF", "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" }, + { + "name": "dz", + "unicode": "1F1E9-1F1FF", + "digest": "cf525d56bac45fe689f92d441274fc0ecbed4f95591d2c066598f72b1ee8d618" + }, { "name": "flag_ea", "unicode": "1F1EA-1F1E6", "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" }, + { + "name": "ea", + "unicode": "1F1EA-1F1E6", + "digest": "1acb13950f7c3692f9a36e618d8ec10a73ead5d7fa80fb52b6b2a18e3d456002" + }, { "name": "flag_ec", "unicode": "1F1EA-1F1E8", "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" }, + { + "name": "ec", + "unicode": "1F1EA-1F1E8", + "digest": "4d9d35450efc6026651ccc2278e70fb90b001ca5e5eecd31361b1e4e23253dbd" + }, { "name": "flag_ee", "unicode": "1F1EA-1F1EA", "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" }, + { + "name": "ee", + "unicode": "1F1EA-1F1EA", + "digest": "86ec7b2f618fe71dddec3d5a621b56b878d683780f1e0ad446f965326d42df48" + }, { "name": "flag_eg", "unicode": "1F1EA-1F1EC", "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" }, + { + "name": "eg", + "unicode": "1F1EA-1F1EC", + "digest": "f06d36a6fec15af4c1a76de30e8469847dde2728bb5a48956b4e466098b778a4" + }, { "name": "flag_eh", "unicode": "1F1EA-1F1ED", "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" }, + { + "name": "eh", + "unicode": "1F1EA-1F1ED", + "digest": "eb63f5b92c62c98dc008dfa7ad8830aa17fa23964f812a28055bd8b6f5960c5b" + }, { "name": "flag_er", "unicode": "1F1EA-1F1F7", "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" }, + { + "name": "er", + "unicode": "1F1EA-1F1F7", + "digest": "e901195f7b37b22a6872d36713de0ec176f6424c209e261e5c849ce318c772f6" + }, { "name": "flag_es", "unicode": "1F1EA-1F1F8", "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" }, + { + "name": "es", + "unicode": "1F1EA-1F1F8", + "digest": "27ab5cc6c2e9f26ccdfa632887533eebcd9b514f80cec9e721cf8e5e2544339c" + }, { "name": "flag_et", "unicode": "1F1EA-1F1F9", "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" }, + { + "name": "et", + "unicode": "1F1EA-1F1F9", + "digest": "6cdb3718c9b3ec713258dd36781db58b7da53f3017445056c1a76233e3b4a7de" + }, { "name": "flag_eu", "unicode": "1F1EA-1F1FA", "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" }, + { + "name": "eu", + "unicode": "1F1EA-1F1FA", + "digest": "363f60e8a747166d5cec8d70bfdf266411eec2ff07933b6187975075caadfd74" + }, { "name": "flag_fi", "unicode": "1F1EB-1F1EE", "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" }, + { + "name": "fi", + "unicode": "1F1EB-1F1EE", + "digest": "1a1959cb551a0e8bdaee8c04657fb7387a4d83173f7759f89468da12e1818a9e" + }, { "name": "flag_fj", "unicode": "1F1EB-1F1EF", "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" }, + { + "name": "fj", + "unicode": "1F1EB-1F1EF", + "digest": "f26dc36ea9c1f32d9bb54874ea384e7118b6e2585be69245fdd73acd8304ae78" + }, { "name": "flag_fk", "unicode": "1F1EB-1F1F0", "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" }, + { + "name": "fk", + "unicode": "1F1EB-1F1F0", + "digest": "0479e233499b704f91a9b13d083e66296efe2f28ed917ab1496b223bfb09adb8" + }, { "name": "flag_fm", "unicode": "1F1EB-1F1F2", "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" }, + { + "name": "fm", + "unicode": "1F1EB-1F1F2", + "digest": "142ea7b4b4a7004329925b495da43ab82351cbaac383c8da6e614b39ba58d05e" + }, { "name": "flag_fo", "unicode": "1F1EB-1F1F4", "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" }, + { + "name": "fo", + "unicode": "1F1EB-1F1F4", + "digest": "f1c800d4f4d39e2aead9a11ed500f16108d6bc48bd24bd2a1af7b966d8e76752" + }, { "name": "flag_fr", "unicode": "1F1EB-1F1F7", "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" }, + { + "name": "fr", + "unicode": "1F1EB-1F1F7", + "digest": "6f52f36b5199c65ab1cad13ff4e77d2d8b48a8ff79b92166976674ffdc7829ee" + }, { "name": "flag_ga", "unicode": "1F1EC-1F1E6", "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" }, + { + "name": "ga", + "unicode": "1F1EC-1F1E6", + "digest": "50a0d5a07466e419b74a4d532738f7958de9baa37df6191be4f3755dccc3b326" + }, { "name": "flag_gb", "unicode": "1F1EC-1F1E7", "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" }, + { + "name": "gb", + "unicode": "1F1EC-1F1E7", + "digest": "220f7da6d5a231b766c79f2e1b7d3fdb74ec0c0c17558cc00a8a8ccdf2afc2e0" + }, { "name": "flag_gd", "unicode": "1F1EC-1F1E9", "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" }, + { + "name": "gd", + "unicode": "1F1EC-1F1E9", + "digest": "3e162b0d13f4ceea7f663b1d425f13863d104e80df75a640f526e276bcd04081" + }, { "name": "flag_ge", "unicode": "1F1EC-1F1EA", "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" }, + { + "name": "ge", + "unicode": "1F1EC-1F1EA", + "digest": "35897f8254675d2efe9e3070c88af9ef214f08440e6ee75ebe81d28cdb57ea2b" + }, { "name": "flag_gf", "unicode": "1F1EC-1F1EB", "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" }, + { + "name": "gf", + "unicode": "1F1EC-1F1EB", + "digest": "3a34df321635f71a0f2cc4e1eda58d85c29230c77456362345196351bf56533d" + }, { "name": "flag_gg", "unicode": "1F1EC-1F1EC", "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" }, + { + "name": "gg", + "unicode": "1F1EC-1F1EC", + "digest": "c972f8d190b4e9ca8890df41503d202ffd73981833d3f3750f563302167bcd66" + }, { "name": "flag_gh", "unicode": "1F1EC-1F1ED", "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" }, + { + "name": "gh", + "unicode": "1F1EC-1F1ED", + "digest": "9c3d3569bd411389fa0af7c6938d4325cedeb9c0e8f059dc1d5a74c6b8d6d01b" + }, { "name": "flag_gi", "unicode": "1F1EC-1F1EE", "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" }, + { + "name": "gi", + "unicode": "1F1EC-1F1EE", + "digest": "ede638bc6fedc30a01821025d87ec19297500da9c04a7a155984fca186118649" + }, { "name": "flag_gl", "unicode": "1F1EC-1F1F1", "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" }, + { + "name": "gl", + "unicode": "1F1EC-1F1F1", + "digest": "a2ce3371eff1da8331671925f707232aa593ac7400d59555c9ca689729ce24ec" + }, { "name": "flag_gm", "unicode": "1F1EC-1F1F2", "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" }, + { + "name": "gm", + "unicode": "1F1EC-1F1F2", + "digest": "932bf6eb75ddd4278268dd2f09d8fffcfef89f8fd6b6e86a08a414cd3ceec94d" + }, { "name": "flag_gn", "unicode": "1F1EC-1F1F3", "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" }, + { + "name": "gn", + "unicode": "1F1EC-1F1F3", + "digest": "ebf543713895adaa09d64897f24bd461191191b8fcbbcede52bdaf4bd2dc67a8" + }, { "name": "flag_gp", "unicode": "1F1EC-1F1F5", "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" }, + { + "name": "gp", + "unicode": "1F1EC-1F1F5", + "digest": "2e6c48d80c571b34f31fa9b3622dcc51e1707c0118e991e9c177742ff02a8a96" + }, { "name": "flag_gq", "unicode": "1F1EC-1F1F6", "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" }, + { + "name": "gq", + "unicode": "1F1EC-1F1F6", + "digest": "b0f5810180d12fc48faf75e73f882dc59072d7bf957f8455bf7e1e336539dc41" + }, { "name": "flag_gr", "unicode": "1F1EC-1F1F7", "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" }, + { + "name": "gr", + "unicode": "1F1EC-1F1F7", + "digest": "8d60d6f8910f5179d851dbea0798b56a492c6be85f3d55e1a1126cd1d6663a3b" + }, { "name": "flag_gs", "unicode": "1F1EC-1F1F8", "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" }, + { + "name": "gs", + "unicode": "1F1EC-1F1F8", + "digest": "7b07915af0e2364ebc386a162d44846f3a7986fdd24e20ad2bc56d64a103fe9c" + }, { "name": "flag_gt", "unicode": "1F1EC-1F1F9", "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" }, + { + "name": "gt", + "unicode": "1F1EC-1F1F9", + "digest": "0c78108ede45bf34917b409a0867f5ec8253c74b694beda083f3e8d04d7a10d8" + }, { "name": "flag_gu", "unicode": "1F1EC-1F1FA", "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" }, + { + "name": "gu", + "unicode": "1F1EC-1F1FA", + "digest": "909f1bc98fa1507adb787eb3875503b21ea937d6ae8bb152153916c2da5e13bb" + }, { "name": "flag_gw", "unicode": "1F1EC-1F1FC", "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" }, + { + "name": "gw", + "unicode": "1F1EC-1F1FC", + "digest": "f5f34410c7b22d5ed9994b47d0e7a9d9a6a1f05c4d3142f7fef3e4409725f5e6" + }, { "name": "flag_gy", "unicode": "1F1EC-1F1FE", "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" }, + { + "name": "gy", + "unicode": "1F1EC-1F1FE", + "digest": "4939cf52ab34a924a31032b42668960a2c7d8d4f998b16b065c247110df334be" + }, { "name": "flag_hk", "unicode": "1F1ED-1F1F0", "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" }, + { + "name": "hk", + "unicode": "1F1ED-1F1F0", + "digest": "bde0916df6d62f6b1cf8f85a8a39526c97fc6ef6fedb0b0cae2adb127a08eafe" + }, { "name": "flag_hm", "unicode": "1F1ED-1F1F2", "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" }, + { + "name": "hm", + "unicode": "1F1ED-1F1F2", + "digest": "603e6c9bff9a0dc941970a313fe98fbf53ff5a57028f1a2766420be4211711cc" + }, { "name": "flag_hn", "unicode": "1F1ED-1F1F3", "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" }, + { + "name": "hn", + "unicode": "1F1ED-1F1F3", + "digest": "2953ad0909bc32c02615f6ad5a4e5f331ba794a41632b1f0fc366e1c640cc2b9" + }, { "name": "flag_hr", "unicode": "1F1ED-1F1F7", "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" }, + { + "name": "hr", + "unicode": "1F1ED-1F1F7", + "digest": "41c9ffc4f0faaa2d77e5cffb781329e7d2489ce879bd8eb9c503621e834abc50" + }, { "name": "flag_ht", "unicode": "1F1ED-1F1F9", "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" }, + { + "name": "ht", + "unicode": "1F1ED-1F1F9", + "digest": "6a56c3d71b4f858e1774aa2134a9f5584087fec968e9ee8bb1046d2ec93bf059" + }, { "name": "flag_hu", "unicode": "1F1ED-1F1FA", "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" }, + { + "name": "hu", + "unicode": "1F1ED-1F1FA", + "digest": "72f5809818d4cab8c0cee73df7f67b820fb8471eea4199911a5917ac099795e8" + }, { "name": "flag_ic", "unicode": "1F1EE-1F1E8", "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" }, + { + "name": "ic", + "unicode": "1F1EE-1F1E8", + "digest": "7e2a7667fcd05f927af47e64c5790c104a9956dd9f1a45f03cb0fdcc85d866d3" + }, { "name": "flag_id", "unicode": "1F1EE-1F1E9", "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" }, + { + "name": "indonesia", + "unicode": "1F1EE-1F1E9", + "digest": "4721f616fae2e443e52f1e9cc96e4835bddca16a2d75d7d5afea57cdee866b7f" + }, { "name": "flag_ie", "unicode": "1F1EE-1F1EA", "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" }, + { + "name": "ie", + "unicode": "1F1EE-1F1EA", + "digest": "84b19833e6c9fb43187f8a28d85045a3df58816f20a07edab90474323174b1f3" + }, { "name": "flag_il", "unicode": "1F1EE-1F1F1", "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" }, + { + "name": "il", + "unicode": "1F1EE-1F1F1", + "digest": "c99d4bd8c2541cf3a7392c4faf4477d96bc47065dd1423b9e06450483e69b34f" + }, { "name": "flag_im", "unicode": "1F1EE-1F1F2", "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" }, + { + "name": "im", + "unicode": "1F1EE-1F1F2", + "digest": "5eeb12c0315b527ce61649a38b64d76af726a73b2d381d1a1ddd1366bafb1bfc" + }, { "name": "flag_in", "unicode": "1F1EE-1F1F3", "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" }, + { + "name": "in", + "unicode": "1F1EE-1F1F3", + "digest": "ecc3cfcff3368fe0875a51a8be9f4dfd449a187e5beb41a2b34241736247f73b" + }, { "name": "flag_io", "unicode": "1F1EE-1F1F4", "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" }, + { + "name": "io", + "unicode": "1F1EE-1F1F4", + "digest": "26243d60e04ba3bc9eb8f008bfc77b2a64bcf1a3d0073eb0449a8c8121618c9c" + }, { "name": "flag_iq", "unicode": "1F1EE-1F1F6", "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" }, + { + "name": "iq", + "unicode": "1F1EE-1F1F6", + "digest": "a1fb5e59575081920b3be5290f654d57a9be099deb56d4ed69eba81a2b531cb3" + }, { "name": "flag_ir", "unicode": "1F1EE-1F1F7", "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" }, + { + "name": "ir", + "unicode": "1F1EE-1F1F7", + "digest": "ab89488b934af1d4bdae7ed16dfc74fffe658bb8e95d5161b48cdd06de44ae85" + }, { "name": "flag_is", "unicode": "1F1EE-1F1F8", "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" }, + { + "name": "is", + "unicode": "1F1EE-1F1F8", + "digest": "55db1fc9e6c56d4c9bcb9a46e5e4300cf2a0c32fa91dc24b487a1d56c8097268" + }, { "name": "flag_it", "unicode": "1F1EE-1F1F9", "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" }, + { + "name": "it", + "unicode": "1F1EE-1F1F9", + "digest": "36fc993fb00ab607578a4d0e573e988e17b9459a68a000a48de905a8238589d0" + }, { "name": "flag_je", "unicode": "1F1EF-1F1EA", "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" }, + { + "name": "je", + "unicode": "1F1EF-1F1EA", + "digest": "c608dbfd1259330e2f8c40dc5d12ffd0489396f4fc5f3ca57bcb2f0d9d05c20c" + }, { "name": "flag_jm", "unicode": "1F1EF-1F1F2", "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" }, + { + "name": "jm", + "unicode": "1F1EF-1F1F2", + "digest": "a8224b68b2d324f848d75e4376875ef76a8174e6ba32790d9ca622fe1eabfd5f" + }, { "name": "flag_jo", "unicode": "1F1EF-1F1F4", "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" }, + { + "name": "jo", + "unicode": "1F1EF-1F1F4", + "digest": "2403563dc2ab4ed0e7e3a0761cc09f96801550bba6b177b54d651d8804ad987d" + }, { "name": "flag_jp", "unicode": "1F1EF-1F1F5", "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" }, + { + "name": "jp", + "unicode": "1F1EF-1F1F5", + "digest": "aea8eebd0a0139818cb7629d9c9a8e55160b458eb8ffeee2f36c5cff4b507fd3" + }, { "name": "flag_ke", "unicode": "1F1F0-1F1EA", "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" }, + { + "name": "ke", + "unicode": "1F1F0-1F1EA", + "digest": "9c8365f74858743bcdce4a9cf6a6f4110faf2dc6433e5dc7d98c24bb3b32a36d" + }, { "name": "flag_kg", "unicode": "1F1F0-1F1EC", "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" }, + { + "name": "kg", + "unicode": "1F1F0-1F1EC", + "digest": "0c72bdb1d64b1e3be3d9516a50655a6162d8501851d2cf2fadb8c6ef7740df4e" + }, { "name": "flag_kh", "unicode": "1F1F0-1F1ED", "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" }, + { + "name": "kh", + "unicode": "1F1F0-1F1ED", + "digest": "49e41e488732d789e395091e144cd6215c6818ba2073e5e22ea21203a737d03c" + }, { "name": "flag_ki", "unicode": "1F1F0-1F1EE", "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" }, + { + "name": "ki", + "unicode": "1F1F0-1F1EE", + "digest": "9d7f168adbcf5f4cfe28470addfdb0a8b231438d593edb70f633981bfa4c7638" + }, { "name": "flag_km", "unicode": "1F1F0-1F1F2", "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" }, + { + "name": "km", + "unicode": "1F1F0-1F1F2", + "digest": "9318c28957fa7a19eba5ec452c1cbce01a5a83d41d29d081614d3abb0585d478" + }, { "name": "flag_kn", "unicode": "1F1F0-1F1F3", "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" }, + { + "name": "kn", + "unicode": "1F1F0-1F1F3", + "digest": "eac7e7d0f023dee5c0c8559bc2c9a96273adda54ce47598025120b30d8d6ebc1" + }, { "name": "flag_kp", "unicode": "1F1F0-1F1F5", "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" }, + { + "name": "kp", + "unicode": "1F1F0-1F1F5", + "digest": "d4d53db6f8363174de6db864c056267ba8a7d7e87b5527f2f42bb9b8ac3f362b" + }, { "name": "flag_kr", "unicode": "1F1F0-1F1F7", "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" }, + { + "name": "kr", + "unicode": "1F1F0-1F1F7", + "digest": "5c7e61ab4a2aae70cbe51f0ca4718516002bc943b35d870bd853a0c98c4e0ed5" + }, { "name": "flag_kw", "unicode": "1F1F0-1F1FC", "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" }, + { + "name": "kw", + "unicode": "1F1F0-1F1FC", + "digest": "5d229cd99d25f4285bd30d98cfcc3cd8346648897476e2905a1811ceeef48d37" + }, { "name": "flag_ky", "unicode": "1F1F0-1F1FE", "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" }, + { + "name": "ky", + "unicode": "1F1F0-1F1FE", + "digest": "9ce3d8dfc273d3a400960876c434b702f93df92c6c00682dbed2ec8e3966d8a8" + }, { "name": "flag_kz", "unicode": "1F1F0-1F1FF", "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" }, + { + "name": "kz", + "unicode": "1F1F0-1F1FF", + "digest": "a6f0be0a767fa4824495d568d9fc2bd8d4c1a26f363873d3b65362e9383e2a50" + }, { "name": "flag_la", "unicode": "1F1F1-1F1E6", "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" }, + { + "name": "la", + "unicode": "1F1F1-1F1E6", + "digest": "ab2ae96da87f7b53ab212f8dcd897a591cff9ea6666270097a8e739ee0b8f8cb" + }, { "name": "flag_lb", "unicode": "1F1F1-1F1E7", "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" }, + { + "name": "lb", + "unicode": "1F1F1-1F1E7", + "digest": "0c3fcab22e9fae1c78658290aff97de785d0b6adb5e3702d00073ce774b7ed54" + }, { "name": "flag_lc", "unicode": "1F1F1-1F1E8", "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" }, + { + "name": "lc", + "unicode": "1F1F1-1F1E8", + "digest": "e154b0b3a1635a36e0d9ad518c0ea12259320e5f1ebbda982248486492065d28" + }, { "name": "flag_li", "unicode": "1F1F1-1F1EE", "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" }, + { + "name": "li", + "unicode": "1F1F1-1F1EE", + "digest": "bbc393a89e73cc8c29a0a9297428d07aa1d4717ea9b7d4dd9d69f21ac7d0605d" + }, { "name": "flag_lk", "unicode": "1F1F1-1F1F0", "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" }, + { + "name": "lk", + "unicode": "1F1F1-1F1F0", + "digest": "376bd501d113a844971ca1006ab31aa086cd55d74842ea5f3dedaba997b58693" + }, { "name": "flag_lr", "unicode": "1F1F1-1F1F7", "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" }, + { + "name": "lr", + "unicode": "1F1F1-1F1F7", + "digest": "9a6ebe1c9d9a53079ee77292a5ad0965f96409b0417f92876a1c3bd463d6a9bc" + }, { "name": "flag_ls", "unicode": "1F1F1-1F1F8", "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" }, + { + "name": "ls", + "unicode": "1F1F1-1F1F8", + "digest": "e2f4b05414f6e0c3d629a92b0534d4145475f0214a83a62c902fe0884c833c89" + }, { "name": "flag_lt", "unicode": "1F1F1-1F1F9", "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" }, + { + "name": "lt", + "unicode": "1F1F1-1F1F9", + "digest": "d5e2f8b2ffa820a33ea6d612fccd61e32467d25154342f5be134d3520e48387f" + }, { "name": "flag_lu", "unicode": "1F1F1-1F1FA", "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" }, + { + "name": "lu", + "unicode": "1F1F1-1F1FA", + "digest": "f43277103292195b51981d08e2dde68eab660a65c7875f510e09a8b2370f1b5c" + }, { "name": "flag_lv", "unicode": "1F1F1-1F1FB", "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" }, + { + "name": "lv", + "unicode": "1F1F1-1F1FB", + "digest": "e1288ac5c80d6e9d577d652e34be247ca39bf9d3d7cfc8a6cae13c1f9ac9dc47" + }, { "name": "flag_ly", "unicode": "1F1F1-1F1FE", "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" }, + { + "name": "ly", + "unicode": "1F1F1-1F1FE", + "digest": "5122294b769a174e3b6e3d238bb846b3e760929f5bb3c1a708d8a429f3f32f68" + }, { "name": "flag_ma", "unicode": "1F1F2-1F1E6", "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" }, + { + "name": "ma", + "unicode": "1F1F2-1F1E6", + "digest": "615a6447ff284de7689b4fd7b04fdda308f65dbbec958cfb96d2977514981d16" + }, { "name": "flag_mc", "unicode": "1F1F2-1F1E8", "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" }, + { + "name": "mc", + "unicode": "1F1F2-1F1E8", + "digest": "08b48b28938acbfc0fbc15c25ee14dbad7164c5165d03df2eee370755ee7b4cf" + }, { "name": "flag_md", "unicode": "1F1F2-1F1E9", "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" }, + { + "name": "md", + "unicode": "1F1F2-1F1E9", + "digest": "93d61de68f821e1e08b30e63d91e8b4a657766475128538894cf9da9a3b4e3c0" + }, { "name": "flag_me", "unicode": "1F1F2-1F1EA", "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" }, + { + "name": "me", + "unicode": "1F1F2-1F1EA", + "digest": "ee55c0eb78241aec2baf1822a47fa46d63209ceae3db7617ae886b823ae229ff" + }, { "name": "flag_mf", "unicode": "1F1F2-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "mf", + "unicode": "1F1F2-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_mg", "unicode": "1F1F2-1F1EC", "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" }, + { + "name": "mg", + "unicode": "1F1F2-1F1EC", + "digest": "86ec8140e2c4854f52cff74757baf0cbb75a4aacca8be6af8c8f9c939a7b866c" + }, { "name": "flag_mh", "unicode": "1F1F2-1F1ED", "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" }, + { + "name": "mh", + "unicode": "1F1F2-1F1ED", + "digest": "8311ea3422c9d5e94b55e19b03bedd6fe6e2a191b7657e15ac75a48932958a5b" + }, { "name": "flag_mk", "unicode": "1F1F2-1F1F0", "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" }, + { + "name": "mk", + "unicode": "1F1F2-1F1F0", + "digest": "5c6f504f88c5a875c06ac8b26fa6e81a9d79c42a1c7d1fad9a5d4c8ad06ca502" + }, { "name": "flag_ml", "unicode": "1F1F2-1F1F1", "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" }, + { + "name": "ml", + "unicode": "1F1F2-1F1F1", + "digest": "d08a4973db40cf28e58ca3c80e8bd4e50d68ba1080b31917aeefdb0e210b5c50" + }, { "name": "flag_mm", "unicode": "1F1F2-1F1F2", "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" }, + { + "name": "mm", + "unicode": "1F1F2-1F1F2", + "digest": "5e95089514ca09bb93afb481b317477c9d053adcf450e0b711d78ed1078c7470" + }, { "name": "flag_mn", "unicode": "1F1F2-1F1F3", "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" }, + { + "name": "mn", + "unicode": "1F1F2-1F1F3", + "digest": "7a0ca72715dd2a36eeeed2f8c888497cb752f0000af8f07d6930743caf6e4273" + }, { "name": "flag_mo", "unicode": "1F1F2-1F1F4", "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" }, + { + "name": "mo", + "unicode": "1F1F2-1F1F4", + "digest": "d2c7c2191bc1bc83d85f2270968cb4de5cf26a11f70e166a8b32c108287ef729" + }, { "name": "flag_mp", "unicode": "1F1F2-1F1F5", "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" }, + { + "name": "mp", + "unicode": "1F1F2-1F1F5", + "digest": "89ad06121fd7981338fe188464491bea371f85125bfb4fc01fb5cad606613b1e" + }, { "name": "flag_mq", "unicode": "1F1F2-1F1F6", "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" }, + { + "name": "mq", + "unicode": "1F1F2-1F1F6", + "digest": "98176f3af823b26a3657a17c5073ee22367898b40bd3973de76329aa87ca5a2e" + }, { "name": "flag_mr", "unicode": "1F1F2-1F1F7", "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" }, + { + "name": "mr", + "unicode": "1F1F2-1F1F7", + "digest": "cc3e705ad84f83fe2d544385c39564743024dab26595d62469b35fdb791f6015" + }, { "name": "flag_ms", "unicode": "1F1F2-1F1F8", "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" }, + { + "name": "ms", + "unicode": "1F1F2-1F1F8", + "digest": "465e3d5700b557f2589bd6e34a0c6b12c634a6ed4dcfbee3c1c841c5de3413f0" + }, { "name": "flag_mt", "unicode": "1F1F2-1F1F9", "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" }, + { + "name": "mt", + "unicode": "1F1F2-1F1F9", + "digest": "e610ba22d8d8ad750ed10dff8e1b4d89bc34f066c3424bfa77dbdc1a5d79743a" + }, { "name": "flag_mu", "unicode": "1F1F2-1F1FA", "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" }, + { + "name": "mu", + "unicode": "1F1F2-1F1FA", + "digest": "3daf015d3b95218677dafbb282b7804686aa68875a6bd1d70c165b7b149e19cb" + }, { "name": "flag_mv", "unicode": "1F1F2-1F1FB", "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" }, + { + "name": "mv", + "unicode": "1F1F2-1F1FB", + "digest": "d30e4bfd04f08177de92f3c175600aaafa89b9668bbe2b83f35f07a74382065c" + }, { "name": "flag_mw", "unicode": "1F1F2-1F1FC", "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" }, + { + "name": "mw", + "unicode": "1F1F2-1F1FC", + "digest": "f364b1c8bfda3f86b5e26422eedc571ba11e312dcc634197631a6840cb22aede" + }, { "name": "flag_mx", "unicode": "1F1F2-1F1FD", "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" }, + { + "name": "mx", + "unicode": "1F1F2-1F1FD", + "digest": "eafb02ec0be9cefab7cef7c426c7d860d98e4947f4da04054154dc86d8f487c4" + }, { "name": "flag_my", "unicode": "1F1F2-1F1FE", "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" }, + { + "name": "my", + "unicode": "1F1F2-1F1FE", + "digest": "9a690b357bc6b970781bd122c1e546ade3ccb73d930c2af1008b82027e36c7cf" + }, { "name": "flag_mz", "unicode": "1F1F2-1F1FF", "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" }, + { + "name": "mz", + "unicode": "1F1F2-1F1FF", + "digest": "36d0548ebfef9e0443ec1d0597ebfa6e95c25b997381f30c8c74008820743bb9" + }, { "name": "flag_na", "unicode": "1F1F3-1F1E6", "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" }, + { + "name": "na", + "unicode": "1F1F3-1F1E6", + "digest": "4989dc9452b0bdfa101cfd3b7c83ef1195a7e45128b9ed00193fe712a6d02fca" + }, { "name": "flag_nc", "unicode": "1F1F3-1F1E8", "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" }, + { + "name": "nc", + "unicode": "1F1F3-1F1E8", + "digest": "7fc9d865eebf729d5496c4cd7576476ec599f65b379d4a6df66b4e399553c2eb" + }, { "name": "flag_ne", "unicode": "1F1F3-1F1EA", "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" }, + { + "name": "ne", + "unicode": "1F1F3-1F1EA", + "digest": "d3f10fb44ec44a04112bc66d05f0a44c6ec46dae73cfd3fe26cdc8b32ec06713" + }, { "name": "flag_nf", "unicode": "1F1F3-1F1EB", "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" }, + { + "name": "nf", + "unicode": "1F1F3-1F1EB", + "digest": "d390e0d52215a025380af221ba9e955e5886edbb4c9f4b124f2fb60a8e019e42" + }, { "name": "flag_ng", "unicode": "1F1F3-1F1EC", "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" }, + { + "name": "nigeria", + "unicode": "1F1F3-1F1EC", + "digest": "e69d1bb8f1db4a0c295c90dda23d8f97c2dea59f9a2da2ecb0e9a1dc4dbea101" + }, { "name": "flag_ni", "unicode": "1F1F3-1F1EE", "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" }, + { + "name": "ni", + "unicode": "1F1F3-1F1EE", + "digest": "dbaccc942637469b0ee75bd5f956958c3c5a89d8f69b69c96f02ab6594124894" + }, { "name": "flag_nl", "unicode": "1F1F3-1F1F1", "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" }, + { + "name": "nl", + "unicode": "1F1F3-1F1F1", + "digest": "bda2eb0315763c3c19d37c664dab1ee4280f20888a0ca57677fd33cfa4240910" + }, { "name": "flag_no", "unicode": "1F1F3-1F1F4", "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" }, + { + "name": "no", + "unicode": "1F1F3-1F1F4", + "digest": "42b49dec756a220781ea271ca8fbcaba524dc3b38d5d8f999bfaa40ef9ebd302" + }, { "name": "flag_np", "unicode": "1F1F3-1F1F5", "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" }, + { + "name": "np", + "unicode": "1F1F3-1F1F5", + "digest": "b5259257db079235310d5d9537d2b5b61ae0326bc8920ba13084b009844e2957" + }, { "name": "flag_nr", "unicode": "1F1F3-1F1F7", "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" }, + { + "name": "nr", + "unicode": "1F1F3-1F1F7", + "digest": "1bd7d1fe2c3a5e98cfd4dff6e8d6dd6d3c74f0051ad615587d77d2291a9784cc" + }, { "name": "flag_nu", "unicode": "1F1F3-1F1FA", "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" }, + { + "name": "nu", + "unicode": "1F1F3-1F1FA", + "digest": "e2a7a398e07d2232147cc0917d72d18b519246d3d314e9f6f03dcf98d312d4ce" + }, { "name": "flag_nz", "unicode": "1F1F3-1F1FF", "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" }, + { + "name": "nz", + "unicode": "1F1F3-1F1FF", + "digest": "ce8b1cb87dae3a3ec865575b57a0b4987a7f4bd3f170e7b210dd764fc2588cd4" + }, { "name": "flag_om", "unicode": "1F1F4-1F1F2", "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" }, + { + "name": "om", + "unicode": "1F1F4-1F1F2", + "digest": "29da72505a276a8a372a00c197388ebc5098c221cab26b3ff755bd62b10f740f" + }, { "name": "flag_pa", "unicode": "1F1F5-1F1E6", "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" }, + { + "name": "pa", + "unicode": "1F1F5-1F1E6", + "digest": "180b673c9aceea43a8b55823a82d80600257e4982d0757d129860e3d8a14f458" + }, { "name": "flag_pe", "unicode": "1F1F5-1F1EA", "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" }, + { + "name": "pe", + "unicode": "1F1F5-1F1EA", + "digest": "b61823ea2cd91e371e40832df5764558b81d44fac41030827a3f6d2564643c00" + }, { "name": "flag_pf", "unicode": "1F1F5-1F1EB", "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" }, + { + "name": "pf", + "unicode": "1F1F5-1F1EB", + "digest": "e560421911f4af90c73a0dbdf8f42e69316003799304c9394fb127e3b83326fa" + }, { "name": "flag_pg", "unicode": "1F1F5-1F1EC", "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" }, + { + "name": "pg", + "unicode": "1F1F5-1F1EC", + "digest": "880e87db2ce0eac38db037683a5db46fd6ce30623cf56ae4a93a747103570044" + }, { "name": "flag_ph", "unicode": "1F1F5-1F1ED", "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" }, + { + "name": "ph", + "unicode": "1F1F5-1F1ED", + "digest": "49aae2f56bfd1385741dc76857aa1f1459778b2d39a1c955e469c5367585bfd5" + }, { "name": "flag_pk", "unicode": "1F1F5-1F1F0", "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" }, + { + "name": "pk", + "unicode": "1F1F5-1F1F0", + "digest": "64379dbfc932df3a07935b5cfa11ca151f761d3728939e982604e12c663cd646" + }, { "name": "flag_pl", "unicode": "1F1F5-1F1F1", "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" }, + { + "name": "pl", + "unicode": "1F1F5-1F1F1", + "digest": "3b688b074c2735d3dea0b7ab74b80eba243ce50cb05d68e585c9d701c1f14617" + }, { "name": "flag_pm", "unicode": "1F1F5-1F1F2", "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" }, + { + "name": "pm", + "unicode": "1F1F5-1F1F2", + "digest": "a13a69ee3131501dd8138173cfb669a35ee8039d84aa665e69dd7f0d0aa3e717" + }, { "name": "flag_pn", "unicode": "1F1F5-1F1F3", "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" }, + { + "name": "pn", + "unicode": "1F1F5-1F1F3", + "digest": "d7ae3985cf66024e4a3001e79a8efbb3e75571f2b0abbd0fb87fc1efc795a2b3" + }, { "name": "flag_pr", "unicode": "1F1F5-1F1F7", "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" }, + { + "name": "pr", + "unicode": "1F1F5-1F1F7", + "digest": "4910dc984bc908158506b770f28af56150cbb4509a4291947dfa2479b9e4b308" + }, { "name": "flag_ps", "unicode": "1F1F5-1F1F8", "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" }, + { + "name": "ps", + "unicode": "1F1F5-1F1F8", + "digest": "b2bca7619fced25de94d7bd398537857460348a552e7d73d189aef3f428e6a13" + }, { "name": "flag_pt", "unicode": "1F1F5-1F1F9", "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" }, + { + "name": "pt", + "unicode": "1F1F5-1F1F9", + "digest": "177282613b4b8b4d9551f1da6a1c3f66f1b96cf67c71c7d164213b26b3237395" + }, { "name": "flag_pw", "unicode": "1F1F5-1F1FC", "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" }, + { + "name": "pw", + "unicode": "1F1F5-1F1FC", + "digest": "2ff42a14bdc7df76b5f989dca381f94765032b26ae47d47b97844abde458cefe" + }, { "name": "flag_py", "unicode": "1F1F5-1F1FE", "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" }, + { + "name": "py", + "unicode": "1F1F5-1F1FE", + "digest": "80169b69a46c4c67d0090dc2c6bf05d1a14f133ac7ae56f811547e8e8f70d81b" + }, { "name": "flag_qa", "unicode": "1F1F6-1F1E6", "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" }, + { + "name": "qa", + "unicode": "1F1F6-1F1E6", + "digest": "589b44b975aa97426afb8db7f8b355491fca246b693903485824bf0f5a6953a2" + }, { "name": "flag_re", "unicode": "1F1F7-1F1EA", "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" }, + { + "name": "re", + "unicode": "1F1F7-1F1EA", + "digest": "77d242261742831a142c9ec74cd17d76b1e6d1af751ff3c6a356646744bc798a" + }, { "name": "flag_ro", "unicode": "1F1F7-1F1F4", "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" }, + { + "name": "ro", + "unicode": "1F1F7-1F1F4", + "digest": "d7d17026ea81f27456983722540f9a23343a3a1b22e7697c4fba118ce8b4719e" + }, { "name": "flag_rs", "unicode": "1F1F7-1F1F8", "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" }, + { + "name": "rs", + "unicode": "1F1F7-1F1F8", + "digest": "e466a18cc0368e623d3fe33a036c1e88db91ae24f7510e17caacc85c41f1bac8" + }, { "name": "flag_ru", "unicode": "1F1F7-1F1FA", "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" }, + { + "name": "ru", + "unicode": "1F1F7-1F1FA", + "digest": "86bf53a62dfc4c434d910f43df70f430fc67c0070fe3fc466c4fbfd6a5d8e646" + }, { "name": "flag_rw", "unicode": "1F1F7-1F1FC", "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" }, + { + "name": "rw", + "unicode": "1F1F7-1F1FC", + "digest": "38ec5a01896c9747a8dbf865d5e8584770e587253b7af3d3b9c36cd993f67518" + }, { "name": "flag_sa", "unicode": "1F1F8-1F1E6", "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" }, + { + "name": "saudiarabia", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, + { + "name": "saudi", + "unicode": "1F1F8-1F1E6", + "digest": "a44d0b145f2a0b68eace24ecfd27519e9525ec764836728bc9c1fe96ccb811a0" + }, { "name": "flag_sb", "unicode": "1F1F8-1F1E7", "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" }, + { + "name": "sb", + "unicode": "1F1F8-1F1E7", + "digest": "8ffa24c5cb92be4dbe43f6cd85b61b9608a3101bd78ebccff4fe99c209b3e241" + }, { "name": "flag_sc", "unicode": "1F1F8-1F1E8", "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" }, + { + "name": "sc", + "unicode": "1F1F8-1F1E8", + "digest": "227d090ac2cbf317e594567b6114b5063a13cfe33abf990d37b200debcfadabb" + }, { "name": "flag_sd", "unicode": "1F1F8-1F1E9", "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" }, + { + "name": "sd", + "unicode": "1F1F8-1F1E9", + "digest": "350f3332e8ea1138e54facc870dd0fea5f2ab7d3fd4baa02ed8627ae79642f6c" + }, { "name": "flag_se", "unicode": "1F1F8-1F1EA", "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" }, + { + "name": "se", + "unicode": "1F1F8-1F1EA", + "digest": "c1b09f36c263727de83b54376f05e083a17a61941af9a1640b826629256a280d" + }, { "name": "flag_sg", "unicode": "1F1F8-1F1EC", "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" }, + { + "name": "sg", + "unicode": "1F1F8-1F1EC", + "digest": "e6fc26920dfc07e4fd3c8d897de9c607e0bf48a3b64a13630c858d707a8e7660" + }, { "name": "flag_sh", "unicode": "1F1F8-1F1ED", "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" }, + { + "name": "sh", + "unicode": "1F1F8-1F1ED", + "digest": "f2c22ab0eb49e3104c35f1c0268b1e63c3a67f41b0cfa9861b189525988e53b6" + }, { "name": "flag_si", "unicode": "1F1F8-1F1EE", "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" }, + { + "name": "si", + "unicode": "1F1F8-1F1EE", + "digest": "1ef0b10e498f71591322f9d8ec122d39838f479370cf7ee922560986ef6c4f2e" + }, { "name": "flag_sj", "unicode": "1F1F8-1F1EF", "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" }, + { + "name": "sj", + "unicode": "1F1F8-1F1EF", + "digest": "ce913b007f84a9cba2add8d754aa791901624c60e4200de426dfa25271cb0f78" + }, { "name": "flag_sk", "unicode": "1F1F8-1F1F0", "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" }, + { + "name": "sk", + "unicode": "1F1F8-1F1F0", + "digest": "d8f8fc4024c82f906effe98facbef9d543fb3708b1134dc502c74dc4a442b30a" + }, { "name": "flag_sl", "unicode": "1F1F8-1F1F1", "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" }, + { + "name": "sl", + "unicode": "1F1F8-1F1F1", + "digest": "dd7fd0452498d8d1c894cf0d5a662ddff9c5bcc02148bdc3dc7e6f25d0bb586e" + }, { "name": "flag_sm", "unicode": "1F1F8-1F1F2", "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" }, + { + "name": "sm", + "unicode": "1F1F8-1F1F2", + "digest": "2b499606aee2b5cbf4037338753c80a4c8f75f4abcef2c8657bd9337e602bbd3" + }, { "name": "flag_sn", "unicode": "1F1F8-1F1F3", "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" }, + { + "name": "sn", + "unicode": "1F1F8-1F1F3", + "digest": "03b46a9d8b129da13f60c23b820b04fba52050ca58a41b859ad57d5c3cc2515d" + }, { "name": "flag_so", "unicode": "1F1F8-1F1F4", "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" }, + { + "name": "so", + "unicode": "1F1F8-1F1F4", + "digest": "ea416b6a05ddc5b16291ebe5101735360b08c834d55ac82c663ac1dd3e459048" + }, { "name": "flag_sr", "unicode": "1F1F8-1F1F7", "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" }, + { + "name": "sr", + "unicode": "1F1F8-1F1F7", + "digest": "012179fbcbcb7343e7b09d33e283fb63c7964a6eca35ccb9407d468e495a9874" + }, { "name": "flag_ss", "unicode": "1F1F8-1F1F8", "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" }, + { + "name": "ss", + "unicode": "1F1F8-1F1F8", + "digest": "6723150482c640643c9dd7e33ea749f4a8b46aceacbd4f5e11aa33b3ee13aab7" + }, { "name": "flag_st", "unicode": "1F1F8-1F1F9", "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" }, + { + "name": "st", + "unicode": "1F1F8-1F1F9", + "digest": "0947fcec2e3cb1b0e9943c3d00891e8ee226e8d0532e9b1fe807ddf2e8fbc49d" + }, { "name": "flag_sv", "unicode": "1F1F8-1F1FB", "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" }, + { + "name": "sv", + "unicode": "1F1F8-1F1FB", + "digest": "ce7e583db833c4b10e2f7a2d09b97bb522c02e96ea0b3f3a48a955f7d8f970d8" + }, { "name": "flag_sx", "unicode": "1F1F8-1F1FD", "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" }, + { + "name": "sx", + "unicode": "1F1F8-1F1FD", + "digest": "c01fb238c7ba439f24a5ef821b6457f2a0fd0b99a1b2d02395bed87f0a4a88e5" + }, { "name": "flag_sy", "unicode": "1F1F8-1F1FE", "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" }, + { + "name": "sy", + "unicode": "1F1F8-1F1FE", + "digest": "a77d87ef98c96140c59998d10d94837e2a056dd3ac5c7522e89e5c62eac69e69" + }, { "name": "flag_sz", "unicode": "1F1F8-1F1FF", "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" }, + { + "name": "sz", + "unicode": "1F1F8-1F1FF", + "digest": "2904ad01040a9107ad556ec4c2561781d96746005cca250babb1127b8ba21050" + }, { "name": "flag_ta", "unicode": "1F1F9-1F1E6", "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" }, + { + "name": "ta", + "unicode": "1F1F9-1F1E6", + "digest": "eda84db90e1a8854e8ff3c15b3b38ee65f7d6532b76970a6fbac304c30d8c959" + }, { "name": "flag_tc", "unicode": "1F1F9-1F1E8", "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" }, + { + "name": "tc", + "unicode": "1F1F9-1F1E8", + "digest": "4628fdf6dc598a2846beefe97f7d4c6812f4961394cec132924b44bbe79b3322" + }, { "name": "flag_td", "unicode": "1F1F9-1F1E9", "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" }, + { + "name": "td", + "unicode": "1F1F9-1F1E9", + "digest": "125ff31e4285cb2a5493a52a2703ebe8e7138b918ec4dae3d0f8693632372df6" + }, { "name": "flag_tf", "unicode": "1F1F9-1F1EB", "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" }, + { + "name": "tf", + "unicode": "1F1F9-1F1EB", + "digest": "489d591e11764ac341f2234020f7879db782b8f673fc9aae425fd713e4082334" + }, { "name": "flag_tg", "unicode": "1F1F9-1F1EC", "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" }, + { + "name": "tg", + "unicode": "1F1F9-1F1EC", + "digest": "4ceedfcfcc22cd14d9add9d86d6748447995f19f7095fa4be883e21eb1aa86bc" + }, { "name": "flag_th", "unicode": "1F1F9-1F1ED", "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" }, + { + "name": "th", + "unicode": "1F1F9-1F1ED", + "digest": "2798cc660af1c5dc4891c30aded3a53d7cfa0af128cc495df8141907b165902d" + }, { "name": "flag_tj", "unicode": "1F1F9-1F1EF", "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" }, + { + "name": "tj", + "unicode": "1F1F9-1F1EF", + "digest": "0483506fc5b5f2d4fc18ea3cd2f8a5da985d68fe4bf90bd3fd05e67e38f32398" + }, { "name": "flag_tk", "unicode": "1F1F9-1F1F0", "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" }, + { + "name": "tk", + "unicode": "1F1F9-1F1F0", + "digest": "d5d4a8c6ce3207731b7c154a9d8d8fa2af055a48f03b3cbbcfd3317d3b8a75f2" + }, { "name": "flag_tl", "unicode": "1F1F9-1F1F1", "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" }, + { + "name": "tl", + "unicode": "1F1F9-1F1F1", + "digest": "7a2ba8f91a6b627c60c88244223a9b9d0c12707f50b174f9c2eca07dd3440df7" + }, { "name": "flag_tm", "unicode": "1F1F9-1F1F2", "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" }, + { + "name": "turkmenistan", + "unicode": "1F1F9-1F1F2", + "digest": "adcf5f23adcf983ce626b44559482f8728251eab34b3ff5d8b125112f3a1010f" + }, { "name": "flag_tn", "unicode": "1F1F9-1F1F3", "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" }, + { + "name": "tn", + "unicode": "1F1F9-1F1F3", + "digest": "5ee690ee1f3c3c0cba9b36efdef902894ec59cefbc60c4baa341efd3d7bb9ba2" + }, { "name": "flag_to", "unicode": "1F1F9-1F1F4", "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" }, + { + "name": "to", + "unicode": "1F1F9-1F1F4", + "digest": "cde8672ca25b0e3a423865283fab9bc3ab10f472e04979b3b2f8032b71e96300" + }, { "name": "flag_tr", "unicode": "1F1F9-1F1F7", "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" }, + { + "name": "tr", + "unicode": "1F1F9-1F1F7", + "digest": "3d83c03ed084cfc81fa633310382acd7213e1eaa19d0ed97d142e7824032b55d" + }, { "name": "flag_tt", "unicode": "1F1F9-1F1F9", "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" }, + { + "name": "tt", + "unicode": "1F1F9-1F1F9", + "digest": "d66d272ac27e2b398289d6b60128ccd3508aeb1f4a00a3920c5e6a21bfe357ed" + }, { "name": "flag_tv", "unicode": "1F1F9-1F1FB", "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" }, + { + "name": "tuvalu", + "unicode": "1F1F9-1F1FB", + "digest": "8716527383854cf1569f737d0f0f9ad77b46747255f24e02f5b2fbc850c2e35c" + }, { "name": "flag_tw", "unicode": "1F1F9-1F1FC", "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" }, + { + "name": "tw", + "unicode": "1F1F9-1F1FC", + "digest": "fb17b97e18e4423c5f60d60ec3ec60b917be579fc4dd9b5b23236786dcb35108" + }, { "name": "flag_tz", "unicode": "1F1F9-1F1FF", "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" }, + { + "name": "tz", + "unicode": "1F1F9-1F1FF", + "digest": "a8a8cf57ae5227cb54620bf31d2d6e154d2067d6d049b8db64bc4e538222948b" + }, { "name": "flag_ua", "unicode": "1F1FA-1F1E6", "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" }, + { + "name": "ua", + "unicode": "1F1FA-1F1E6", + "digest": "03aca4b3ffd60d944a5793eb7530f8d8ae527782f642f6606194e46ee314b12c" + }, { "name": "flag_ug", "unicode": "1F1FA-1F1EC", "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" }, + { + "name": "ug", + "unicode": "1F1FA-1F1EC", + "digest": "70226a1585e88390b3b815b8b79a0ddb36d2961c6b465c4ff72aa444abfe982e" + }, { "name": "flag_um", "unicode": "1F1FA-1F1F2", "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" }, + { + "name": "um", + "unicode": "1F1FA-1F1F2", + "digest": "aa83bf051149acf907140a860de5de1700710e4164ae5549ad1040b24d0a142b" + }, { "name": "flag_us", "unicode": "1F1FA-1F1F8", "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" }, + { + "name": "us", + "unicode": "1F1FA-1F1F8", + "digest": "32ba2aa09a30514247e91d60762791b582f547a37d9151f98b700dff50f355ea" + }, { "name": "flag_uy", "unicode": "1F1FA-1F1FE", "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" }, + { + "name": "uy", + "unicode": "1F1FA-1F1FE", + "digest": "0e01b3f1df4bdf6d616dacc9c5825151b941bf074be750e8b24a07ea5d5bcacb" + }, { "name": "flag_uz", "unicode": "1F1FA-1F1FF", "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" }, + { + "name": "uz", + "unicode": "1F1FA-1F1FF", + "digest": "903029ce83812a2134f24b65db35b183443a440ea5fecaa6ef7dcaaf65b2519c" + }, { "name": "flag_va", "unicode": "1F1FB-1F1E6", "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" }, + { + "name": "va", + "unicode": "1F1FB-1F1E6", + "digest": "fd3c1c5d0ac030e838f807288912c98a3e258f87901e252e46942a4dab9f8cb7" + }, { "name": "flag_vc", "unicode": "1F1FB-1F1E8", "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" }, + { + "name": "vc", + "unicode": "1F1FB-1F1E8", + "digest": "7cd554ea8ca817b5366701160274587ab44167ae5a89c430bbaf237ea18b7421" + }, { "name": "flag_ve", "unicode": "1F1FB-1F1EA", "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" }, + { + "name": "ve", + "unicode": "1F1FB-1F1EA", + "digest": "72930094fb088c1facabea07616035ec4771374358a90c3045219d087b350dd8" + }, { "name": "flag_vg", "unicode": "1F1FB-1F1EC", "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" }, + { + "name": "vg", + "unicode": "1F1FB-1F1EC", + "digest": "78a59afd368b7a8312bfdb2f49927ff09e6b8f46aab0136c0453e3319e81df49" + }, { "name": "flag_vi", "unicode": "1F1FB-1F1EE", "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" }, + { + "name": "vi", + "unicode": "1F1FB-1F1EE", + "digest": "e070879f9605a9bae66bb84f2abf5a40c8b264baee65cd4f7a6720b826739f29" + }, { "name": "flag_vn", "unicode": "1F1FB-1F1F3", "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" }, + { + "name": "vn", + "unicode": "1F1FB-1F1F3", + "digest": "100ddf06e0f239b170f4d6cb459450bf4945281ee818f7d3c061828b80562219" + }, { "name": "flag_vu", "unicode": "1F1FB-1F1FA", "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" }, + { + "name": "vu", + "unicode": "1F1FB-1F1FA", + "digest": "59fc9d16818295bba4f7f551598f85378cd07f2bd7e31a4eef2589aaa3847563" + }, { "name": "flag_wf", "unicode": "1F1FC-1F1EB", "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" }, + { + "name": "wf", + "unicode": "1F1FC-1F1EB", + "digest": "62627702e3e3768808c12f153a527ffcc492ad74d8cdc1858cfde971efd0c8ee" + }, { "name": "flag_white", "unicode": "1F3F3", "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" }, + { + "name": "waving_white_flag", + "unicode": "1F3F3", + "digest": "96307e3a28e92d1e7147a06f154ffc291ee3cd1765cf8b7bfb06412294112559" + }, { "name": "flag_ws", "unicode": "1F1FC-1F1F8", "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" }, + { + "name": "ws", + "unicode": "1F1FC-1F1F8", + "digest": "0c95271d0f4b23f0d215ee0fba05cf08ecb70665d4c028e17463ecda2754b164" + }, { "name": "flag_xk", "unicode": "1F1FD-1F1F0", "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" }, + { + "name": "xk", + "unicode": "1F1FD-1F1F0", + "digest": "713aa7d228e96f4a06d58d1fb8c2a55296c3e56842f8177ca936f3e09f50da1e" + }, { "name": "flag_ye", "unicode": "1F1FE-1F1EA", "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" }, + { + "name": "ye", + "unicode": "1F1FE-1F1EA", + "digest": "3bb65bae9c913357bcae8b8b5878efc9e194ca308442ab69639c29716b49f078" + }, { "name": "flag_yt", "unicode": "1F1FE-1F1F9", "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" }, + { + "name": "yt", + "unicode": "1F1FE-1F1F9", + "digest": "f86c86f4c194610a3af78971fcf221ad97b9499d08f6d64476e417a2f52a611e" + }, { "name": "flag_za", "unicode": "1F1FF-1F1E6", "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" }, + { + "name": "za", + "unicode": "1F1FF-1F1E6", + "digest": "4dd4fa49a01fdcfc7c1c099a7869e0e9acba83a6a3debf6c8505ada4c796b872" + }, { "name": "flag_zm", "unicode": "1F1FF-1F1F2", "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" }, + { + "name": "zm", + "unicode": "1F1FF-1F1F2", + "digest": "ab6790d89875447de3d1c7f4713b102761bc3e9afdd714b818689e175ca03011" + }, { "name": "flag_zw", "unicode": "1F1FF-1F1FC", "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" }, + { + "name": "zw", + "unicode": "1F1FF-1F1FC", + "digest": "9d39b934fe922174b2250f2cd1b174a548d2904091d3298f35b7cc59fbceb181" + }, { "name": "flags", "unicode": "1F38F", @@ -3669,11 +5304,21 @@ "unicode": "1F581", "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" }, + { + "name": "clamshell_mobile_phone", + "unicode": "1F581", + "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697" + }, { "name": "floppy_black", "unicode": "1F5AA", "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" }, + { + "name": "black_hard_shell_floppy_disk", + "unicode": "1F5AA", + "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea" + }, { "name": "floppy_disk", "unicode": "1F4BE", @@ -3684,6 +5329,11 @@ "unicode": "1F5AB", "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" }, + { + "name": "white_hard_shell_floppy_disk", + "unicode": "1F5AB", + "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0" + }, { "name": "flower_playing_cards", "unicode": "1F3B4", @@ -3714,6 +5364,11 @@ "unicode": "1F5C1", "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" }, + { + "name": "open_folder", + "unicode": "1F5C1", + "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685" + }, { "name": "football", "unicode": "1F3C8", @@ -3734,6 +5389,11 @@ "unicode": "1F37D", "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" }, + { + "name": "fork_and_knife_with_plate", + "unicode": "1F37D", + "digest": "b4081b9edea6cdab5112fdd17535051ba17710953013f5020c7c40f84a1e3247" + }, { "name": "fountain", "unicode": "26F2", @@ -3754,16 +5414,31 @@ "unicode": "1F5BC", "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" }, + { + "name": "frame_with_picture", + "unicode": "1F5BC", + "digest": "6ff21063063989c6ae7dd69f4d6a781c676f9dba380d8e6f1dbac5d53b24f349" + }, { "name": "frame_tiles", "unicode": "1F5BD", "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" }, + { + "name": "frame_with_tiles", + "unicode": "1F5BD", + "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0" + }, { "name": "frame_x", "unicode": "1F5BE", "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" }, + { + "name": "frame_with_an_x", + "unicode": "1F5BE", + "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15" + }, { "name": "free", "unicode": "1F193", @@ -3789,11 +5464,21 @@ "unicode": "1F626", "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" }, + { + "name": "anguished", + "unicode": "1F626", + "digest": "fb39f5c2aea98054adb02a3a0ac34a2e38d83f32cd590e9d2449e06a9702f2f5" + }, { "name": "frowning2", "unicode": "2639", "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" }, + { + "name": "white_frowning_face", + "unicode": "2639", + "digest": "7bb6c682a6c9f98bf3a5ae986e317fd26d1af497c857500deec2f06b6a3af5da" + }, { "name": "fuelpump", "unicode": "26FD", @@ -4029,6 +5714,11 @@ "unicode": "2692", "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" }, + { + "name": "hammer_and_pick", + "unicode": "2692", + "digest": "2e4fe33406ca03fbb0df1596d63e903d8ee6bd78ecc3ec38a67dd2cecbc584e2" + }, { "name": "hamster", "unicode": "1F439", @@ -4039,41 +5729,81 @@ "unicode": "1F590", "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" }, + { + "name": "raised_hand_with_fingers_splayed", + "unicode": "1F590", + "digest": "a43e52f7cdec5e9d51497888b0988d7bbd42846ad7e492b196293fbce576d197" + }, { "name": "hand_splayed_reverse", "unicode": "1F591", "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" }, + { + "name": "reversed_raised_hand_with_fingers_splayed", + "unicode": "1F591", + "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db" + }, { "name": "hand_splayed_tone1", "unicode": "1F590-1F3FB", "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" }, + { + "name": "raised_hand_with_fingers_splayed_tone1", + "unicode": "1F590-1F3FB", + "digest": "73cceec7117280d330f8a149979190f0f355dd8d0a92821be89fb70344bb8dfe" + }, { "name": "hand_splayed_tone2", "unicode": "1F590-1F3FC", "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" }, + { + "name": "raised_hand_with_fingers_splayed_tone2", + "unicode": "1F590-1F3FC", + "digest": "b06fac698128f4c3a7b8ea56e8bc4de088bb5461aa0f9c84553f16b43d347145" + }, { "name": "hand_splayed_tone3", "unicode": "1F590-1F3FD", "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" }, + { + "name": "raised_hand_with_fingers_splayed_tone3", + "unicode": "1F590-1F3FD", + "digest": "a94ee9a2f8cdec6d2f7dd6887d1c7b8e064fcad63030c2c7c001742d72b5603e" + }, { "name": "hand_splayed_tone4", "unicode": "1F590-1F3FE", "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" }, + { + "name": "raised_hand_with_fingers_splayed_tone4", + "unicode": "1F590-1F3FE", + "digest": "501792b4126c6f32e755accee0fc8b4d1915e1d36c4ceaa40f3bd0066efe76c3" + }, { "name": "hand_splayed_tone5", "unicode": "1F590-1F3FF", "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" }, + { + "name": "raised_hand_with_fingers_splayed_tone5", + "unicode": "1F590-1F3FF", + "digest": "22ed533d587cf44f286e2d6ad77be20b4b5f133c422af4ca51e9af86a75002d8" + }, { "name": "hand_victory", "unicode": "1F594", "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" }, + { + "name": "reversed_victory_hand", + "unicode": "1F594", + "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735" + }, { "name": "handbag", "unicode": "1F45C", @@ -4104,6 +5834,11 @@ "unicode": "1F915", "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" }, + { + "name": "face_with_head_bandage", + "unicode": "1F915", + "digest": "d690b740ff4f58e89dfc764c6411a4e84cfedffd7694eb5efa839a642dbabd08" + }, { "name": "headphones", "unicode": "1F3A7", @@ -4129,6 +5864,11 @@ "unicode": "2763", "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" }, + { + "name": "heavy_heart_exclamation_mark_ornament", + "unicode": "2763", + "digest": "9751c89dcf10805f2011949ff3ddcb6bcb13de8c32ae5de9e03955e8a4235df2" + }, { "name": "heart_eyes", "unicode": "1F60D", @@ -4144,6 +5884,11 @@ "unicode": "1F394", "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" }, + { + "name": "heart_with_tip_on_the_left", + "unicode": "1F394", + "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204" + }, { "name": "heartbeat", "unicode": "1F493", @@ -4199,6 +5944,11 @@ "unicode": "26D1", "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" }, + { + "name": "helmet_with_white_cross", + "unicode": "26D1", + "digest": "affbe9dd87b87ff9235b4858c59c2a73e9ed30dd5221e5b666b8d7747378a9c4" + }, { "name": "herb", "unicode": "1F33F", @@ -4234,6 +5984,11 @@ "unicode": "1F3D8", "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" }, + { + "name": "house_buildings", + "unicode": "1F3D8", + "digest": "9980d6dd6cbd23b820747ecac4cb10974dd24b0c94b4acfe21fa87793ad065c9" + }, { "name": "honey_pot", "unicode": "1F36F", @@ -4289,6 +6044,11 @@ "unicode": "1F32D", "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" }, + { + "name": "hot_dog", + "unicode": "1F32D", + "digest": "58b829e26b5c4642942898d9c7873cb08e048fd7deaacba8292899d5d895cb2b" + }, { "name": "hotel", "unicode": "1F3E8", @@ -4319,6 +6079,11 @@ "unicode": "1F3DA", "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" }, + { + "name": "derelict_house_building", + "unicode": "1F3DA", + "digest": "e404631e3a296bdeae3de7510da8934c32327bc0fa0f7ae4e676b61932165668" + }, { "name": "house_with_garden", "unicode": "1F3E1", @@ -4329,6 +6094,11 @@ "unicode": "1F917", "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" }, + { + "name": "hugging_face", + "unicode": "1F917", + "digest": "68ed6c4e0eae9071cf67770a39e07a2290b4f7763170f765b3cd3ac67ae43240" + }, { "name": "hushed", "unicode": "1F62F", @@ -4379,6 +6149,11 @@ "unicode": "1F6C8", "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" }, + { + "name": "circled_information_source", + "unicode": "1F6C8", + "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca" + }, { "name": "information_desk_person", "unicode": "1F481", @@ -4434,6 +6209,11 @@ "unicode": "1F3DD", "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" }, + { + "name": "desert_island", + "unicode": "1F3DD", + "digest": "17f02b309b62ed9542b1d8943168302846040e420f413e56d799bb5fba7064fa" + }, { "name": "izakaya_lantern", "unicode": "1F3EE", @@ -4474,6 +6254,11 @@ "unicode": "1F6E6", "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" }, + { + "name": "up_pointing_military_airplane", + "unicode": "1F6E6", + "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948" + }, { "name": "joy", "unicode": "1F602", @@ -4504,21 +6289,41 @@ "unicode": "1F5DD", "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" }, + { + "name": "old_key", + "unicode": "1F5DD", + "digest": "87a7d42531d7a11dcb11b0d6d1be611ee8cec35b5d22226a8ac6083fedef4f5d" + }, { "name": "keyboard", "unicode": "1F5AE", "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" }, + { + "name": "wired_keyboard", + "unicode": "1F5AE", + "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16" + }, { "name": "keyboard_mouse", "unicode": "1F5A6", "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" }, + { + "name": "keyboard_and_mouse", + "unicode": "1F5A6", + "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3" + }, { "name": "keyboard_with_jacks", "unicode": "1F398", "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" }, + { + "name": "musical_keyboard_with_jacks", + "unicode": "1F398", + "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee" + }, { "name": "keycap_ten", "unicode": "1F51F", @@ -4539,11 +6344,21 @@ "unicode": "1F468-2764-1F48B-1F468", "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" }, + { + "name": "couplekiss_mm", + "unicode": "1F468-2764-1F48B-1F468", + "digest": "381364ad988ec07cc3708fd60f71838092224009088fff587069b4e8ab01ee63" + }, { "name": "kiss_ww", "unicode": "1F469-2764-1F48B-1F469", "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" }, + { + "name": "couplekiss_ww", + "unicode": "1F469-2764-1F48B-1F469", + "digest": "7705ca707b73f44c856ea324bdfe30ed05244c8d192d1111f6e1d62ab3f2f8a5" + }, { "name": "kissing", "unicode": "1F617", @@ -4619,6 +6434,11 @@ "unicode": "1F606", "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" }, + { + "name": "satisfied", + "unicode": "1F606", + "digest": "f22d3be77f1daf058d04c3cbc1fd7f76b4dc069d2d300b45e63e768b08d269c5" + }, { "name": "leaves", "unicode": "1F343", @@ -4639,6 +6459,11 @@ "unicode": "1F57B", "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" }, + { + "name": "left_hand_telephone_receiver", + "unicode": "1F57B", + "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3" + }, { "name": "left_right_arrow", "unicode": "2194", @@ -4674,6 +6499,11 @@ "unicode": "1F574", "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" }, + { + "name": "man_in_business_suit_levitating", + "unicode": "1F574", + "digest": "3e4e9a5ac6a8dbd7909c58a9d915f16f1a0fc59cc019714ae5935f18e4704044" + }, { "name": "libra", "unicode": "264E", @@ -4684,36 +6514,71 @@ "unicode": "1F3CB", "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" }, + { + "name": "weight_lifter", + "unicode": "1F3CB", + "digest": "f64db037fd21e5918e5de35d6a561ef4b44668e307ed351338de00fcf3e771e3" + }, { "name": "lifter_tone1", "unicode": "1F3CB-1F3FB", "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" }, + { + "name": "weight_lifter_tone1", + "unicode": "1F3CB-1F3FB", + "digest": "f9e0d161b12c4908ac3409b11c1a77ee38f33ba018f12416545876214bfb7c01" + }, { "name": "lifter_tone2", "unicode": "1F3CB-1F3FC", "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" }, + { + "name": "weight_lifter_tone2", + "unicode": "1F3CB-1F3FC", + "digest": "631eb6ed5bd147dc6f1f8b94149abe44d62a0f78e7809e37a4bfe127c40ed98f" + }, { "name": "lifter_tone3", "unicode": "1F3CB-1F3FD", "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" }, + { + "name": "weight_lifter_tone3", + "unicode": "1F3CB-1F3FD", + "digest": "406b5707a47d9066f016acf0b64fa695e3505acc2453758a0428de21efd7eb6d" + }, { "name": "lifter_tone4", "unicode": "1F3CB-1F3FE", "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" }, + { + "name": "weight_lifter_tone4", + "unicode": "1F3CB-1F3FE", + "digest": "d917164ed8c4bb1ffcc887ca256ec329e7fa1b9516eaf8c159f8b43fdb071ed6" + }, { "name": "lifter_tone5", "unicode": "1F3CB-1F3FF", "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" }, + { + "name": "weight_lifter_tone5", + "unicode": "1F3CB-1F3FF", + "digest": "f79ea93e8a40b3c895b693bf49eb4ce6e7b3f4413595e5881ea44839fd7fe8e5" + }, { "name": "light_check_mark", "unicode": "1F5F8", "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" }, + { + "name": "light_mark", + "unicode": "1F5F8", + "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b" + }, { "name": "light_rail", "unicode": "1F688", @@ -4729,6 +6594,11 @@ "unicode": "1F981", "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" }, + { + "name": "lion", + "unicode": "1F981", + "digest": "935b1076815f51fafcd860a395d0a03c536acfcea61ffcf542a377da046fa7d9" + }, { "name": "lips", "unicode": "1F444", @@ -4929,6 +6799,11 @@ "unicode": "1F5FA", "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" }, + { + "name": "world_map", + "unicode": "1F5FA", + "digest": "f56116d09996d6d08fb5cdfb46622b545253f2649008170fc2011a9713fa875b" + }, { "name": "maple_leaf", "unicode": "1F341", @@ -4979,6 +6854,11 @@ "unicode": "1F3C5", "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" }, + { + "name": "sports_medal", + "unicode": "1F3C5", + "digest": "270d438b6e2155e944dc734ea3e4d02409e51f59db2db636398fbf96e5edb0e6" + }, { "name": "mega", "unicode": "1F4E3", @@ -5004,31 +6884,61 @@ "unicode": "1F918", "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" }, + { + "name": "sign_of_the_horns", + "unicode": "1F918", + "digest": "45e5fac0b9b019cf217dcfd1380cafb0d03063454612178278dac1ca5f8476a6" + }, { "name": "metal_tone1", "unicode": "1F918-1F3FB", "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" }, + { + "name": "sign_of_the_horns_tone1", + "unicode": "1F918-1F3FB", + "digest": "9b3596fe7c063df838f0a43fb680ce10fb88e2b73c5c3324abfa357a224c17aa" + }, { "name": "metal_tone2", "unicode": "1F918-1F3FC", "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" }, + { + "name": "sign_of_the_horns_tone2", + "unicode": "1F918-1F3FC", + "digest": "e15a4898a0efca4354ac48d6b01ff0618ce8b110b1246a4f5d78e19b54658be6" + }, { "name": "metal_tone3", "unicode": "1F918-1F3FD", "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" }, + { + "name": "sign_of_the_horns_tone3", + "unicode": "1F918-1F3FD", + "digest": "c159e8179cb1907c246b432d87c5253b914fd7cebb6ac05292c4e38eff4815b0" + }, { "name": "metal_tone4", "unicode": "1F918-1F3FE", "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" }, + { + "name": "sign_of_the_horns_tone4", + "unicode": "1F918-1F3FE", + "digest": "a8a43a88028c97074321e3da56df1045db41ede58bf286c21d7ae90f222f2011" + }, { "name": "metal_tone5", "unicode": "1F918-1F3FF", "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" }, + { + "name": "sign_of_the_horns_tone5", + "unicode": "1F918-1F3FF", + "digest": "e6611e826e867e2c73a8cadb138e4aa6365e3583dd229ff24b3e8f161904bf56" + }, { "name": "metro", "unicode": "1F687", @@ -5044,6 +6954,11 @@ "unicode": "1F399", "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" }, + { + "name": "studio_microphone", + "unicode": "1F399", + "digest": "f9df32cd207808f67a895d3460a215d1ecc42e377907bcd64731c02b697d4f32" + }, { "name": "microscope", "unicode": "1F52C", @@ -5054,31 +6969,61 @@ "unicode": "1F595", "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" }, + { + "name": "reversed_hand_with_middle_finger_extended", + "unicode": "1F595", + "digest": "c6320b236a4a9593aeade511b52dd3114207e947458cb3b818c78737a505fdf6" + }, { "name": "middle_finger_tone1", "unicode": "1F595-1F3FB", "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone1", + "unicode": "1F595-1F3FB", + "digest": "93c7aa994856185519d576cb779bdcff3a33f7077eef98e70968125f92f02448" + }, { "name": "middle_finger_tone2", "unicode": "1F595-1F3FC", "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone2", + "unicode": "1F595-1F3FC", + "digest": "a0de802294717b80e08d9d30f5fd64eacb90b5b3b9d7a0c27d6226a22822597f" + }, { "name": "middle_finger_tone3", "unicode": "1F595-1F3FD", "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone3", + "unicode": "1F595-1F3FD", + "digest": "8bbbab07c838257416bbf8377904362c07019fca9d5abf9fd048ccf6370178da" + }, { "name": "middle_finger_tone4", "unicode": "1F595-1F3FE", "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone4", + "unicode": "1F595-1F3FE", + "digest": "d9eed8db540fdb669c6ae5ef168b77659660589f5ddd9b66062274d335a3ef04" + }, { "name": "middle_finger_tone5", "unicode": "1F595-1F3FF", "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" }, + { + "name": "reversed_hand_with_middle_finger_extended_tone5", + "unicode": "1F595-1F3FF", + "digest": "0519c3298040e57db202294476df239edb9b23b44848bab296bc45eda7cf8664" + }, { "name": "military_medal", "unicode": "1F396", @@ -5109,6 +7054,11 @@ "unicode": "1F911", "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" }, + { + "name": "money_mouth_face", + "unicode": "1F911", + "digest": "3ac2f9b5409e1426eef6966938ca04cf78aeffefd43f44b6c86af4af7836e22f" + }, { "name": "money_with_wings", "unicode": "1F4B8", @@ -5144,11 +7094,21 @@ "unicode": "1F5F1", "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" }, + { + "name": "lightning_mood_bubble", + "unicode": "1F5F1", + "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9" + }, { "name": "mood_lightning", "unicode": "1F5F2", "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" }, + { + "name": "lightning_mood", + "unicode": "1F5F2", + "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f" + }, { "name": "mortar_board", "unicode": "1F393", @@ -5169,6 +7129,11 @@ "unicode": "1F3CD", "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" }, + { + "name": "racing_motorcycle", + "unicode": "1F3CD", + "digest": "8429fb6dfeb873abdffcc179c32d4f23e91c9e6b27b06cd204fd2e83cc11189e" + }, { "name": "motorway", "unicode": "1F6E3", @@ -5229,6 +7194,11 @@ "unicode": "1F3D4", "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" }, + { + "name": "snow_capped_mountain", + "unicode": "1F3D4", + "digest": "9939aade3d4d972ba3af16fcc6cc2454978f5426e4c92838734a44db065ce0ff" + }, { "name": "mouse", "unicode": "1F42D", @@ -5244,11 +7214,21 @@ "unicode": "1F5AF", "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" }, + { + "name": "one_button_mouse", + "unicode": "1F5AF", + "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0" + }, { "name": "mouse_three_button", "unicode": "1F5B1", "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" }, + { + "name": "three_button_mouse", + "unicode": "1F5B1", + "digest": "6a5629fee01145211cc8f4e8f59c5f1e61affed38c650502213d76c7d8861b01" + }, { "name": "movie_camera", "unicode": "1F3A5", @@ -5364,11 +7344,21 @@ "unicode": "1F913", "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" }, + { + "name": "nerd_face", + "unicode": "1F913", + "digest": "94efd551700aae8909b8dd7a78a54a33e070d24b2e0a10534353645084614e98" + }, { "name": "network", "unicode": "1F5A7", "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" }, + { + "name": "three_networked_computers", + "unicode": "1F5A7", + "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06" + }, { "name": "neutral_face", "unicode": "1F610", @@ -5399,6 +7389,11 @@ "unicode": "1F5DE", "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" }, + { + "name": "rolled_up_newspaper", + "unicode": "1F5DE", + "digest": "0ca6b5850091f23295c970815a8e64a52e3c3dae492029ecb1e0726c2693f9bf" + }, { "name": "ng", "unicode": "1F196", @@ -5524,11 +7519,21 @@ "unicode": "1F5C9", "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" }, + { + "name": "note_page", + "unicode": "1F5C9", + "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af" + }, { "name": "note_empty", "unicode": "1F5C6", "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" }, + { + "name": "empty_note_page", + "unicode": "1F5C6", + "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be" + }, { "name": "notebook", "unicode": "1F4D3", @@ -5544,16 +7549,31 @@ "unicode": "1F5CA", "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" }, + { + "name": "note_pad", + "unicode": "1F5CA", + "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e" + }, { "name": "notepad_empty", "unicode": "1F5C7", "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" }, + { + "name": "empty_note_pad", + "unicode": "1F5C7", + "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af" + }, { "name": "notepad_spiral", "unicode": "1F5D2", "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" }, + { + "name": "spiral_note_pad", + "unicode": "1F5D2", + "digest": "c181b6c1cc6063ec1848e46cbbf1d8b890c53b59cdc5218311ce06889570e727" + }, { "name": "notes", "unicode": "1F3B6", @@ -5599,6 +7619,11 @@ "unicode": "1F6E2", "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" }, + { + "name": "oil_drum", + "unicode": "1F6E2", + "digest": "f8b7626cb09e229203105b9c8c7f3fbb38c0650021092fc50115ad517248644a" + }, { "name": "ok", "unicode": "1F197", @@ -5699,31 +7724,61 @@ "unicode": "1F475", "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" }, + { + "name": "grandma", + "unicode": "1F475", + "digest": "3ed599443eed25399aac999fc234c9e97f8fb6ec567e37a553c26e01021b097c" + }, { "name": "older_woman_tone1", "unicode": "1F475-1F3FB", "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" }, + { + "name": "grandma_tone1", + "unicode": "1F475-1F3FB", + "digest": "7421c5dba67cfd1eeabb2fa8faf4aa0d615d23f191cf7d7c0ad9c1fa884edfda" + }, { "name": "older_woman_tone2", "unicode": "1F475-1F3FC", "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" }, + { + "name": "grandma_tone2", + "unicode": "1F475-1F3FC", + "digest": "65edeef25648ac7f8be535df06af1286441691fa15176e99a6e83fc779aa2cde" + }, { "name": "older_woman_tone3", "unicode": "1F475-1F3FD", "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" }, + { + "name": "grandma_tone3", + "unicode": "1F475-1F3FD", + "digest": "5d27bbcc5796227a9caec1c7612d3f691055655b96f7303e420839463d76c269" + }, { "name": "older_woman_tone4", "unicode": "1F475-1F3FE", "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" }, + { + "name": "grandma_tone4", + "unicode": "1F475-1F3FE", + "digest": "75b858e910175fc0233503d672120fd43ac035ba3fd2052fbb44df39f6e3695c" + }, { "name": "older_woman_tone5", "unicode": "1F475-1F3FF", "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" }, + { + "name": "grandma_tone5", + "unicode": "1F475-1F3FF", + "digest": "9da1cf10a605c470877d7f4a840f99344b1ec2e7b1ec7db61e930cde77025e3b" + }, { "name": "om_symbol", "unicode": "1F549", @@ -5809,6 +7864,11 @@ "unicode": "1F5B8", "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" }, + { + "name": "optical_disc_icon", + "unicode": "1F5B8", + "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d" + }, { "name": "orange_book", "unicode": "1F4D9", @@ -5864,6 +7924,11 @@ "unicode": "1F58C", "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" }, + { + "name": "lower_left_paintbrush", + "unicode": "1F58C", + "digest": "73eb33184f5f495d6c2699fafc1a8680069f82a70fbe519290c3a2ce30d1aee9" + }, { "name": "palm_tree", "unicode": "1F334", @@ -5884,11 +7949,21 @@ "unicode": "1F587", "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" }, + { + "name": "linked_paperclips", + "unicode": "1F587", + "digest": "7071e031f4a100c3cb3573fbfa375360043f0276289a0818f2ffaf71b3580040" + }, { "name": "park", "unicode": "1F3DE", "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" }, + { + "name": "national_park", + "unicode": "1F3DE", + "digest": "d257f0f1b1a0134573f80ba1a5f522a91c320ee7f93a1cb64877c077e7e19b50" + }, { "name": "parking", "unicode": "1F17F", @@ -5914,11 +7989,21 @@ "unicode": "23F8", "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" }, + { + "name": "double_vertical_bar", + "unicode": "23F8", + "digest": "edd605ffaa39a7905ed0958b7cc69f00f5b271e579198d2df1746ad1b3648272" + }, { "name": "peace", "unicode": "262E", "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" }, + { + "name": "peace_symbol", + "unicode": "262E", + "digest": "e0ee8a5c9fb18d5db6841b21527ed8fd955abdff9ffdb7b2684dca22107015fc" + }, { "name": "peach", "unicode": "1F351", @@ -5934,16 +8019,31 @@ "unicode": "1F58A", "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" }, + { + "name": "lower_left_ballpoint_pen", + "unicode": "1F58A", + "digest": "6becdc6f622c774bb09b7e7592bba2123ecccc9de32a35f0b18b50d7d54109cb" + }, { "name": "pen_fountain", "unicode": "1F58B", "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" }, + { + "name": "lower_left_fountain_pen", + "unicode": "1F58B", + "digest": "8c78cf0c2bd1d5e309d2d3356ff207e3fc76ca18dd6b90762cb62f6afbc95c6a" + }, { "name": "pencil", "unicode": "1F4DD", "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" }, + { + "name": "memo", + "unicode": "1F4DD", + "digest": "62b7ee5d9352114d09ee6f2c9a4c5e8b79f775a6c509e82ddfcdd61e13716249" + }, { "name": "pencil2", "unicode": "270F", @@ -5954,6 +8054,11 @@ "unicode": "1F589", "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" }, + { + "name": "lower_left_pencil", + "unicode": "1F589", + "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c" + }, { "name": "penguin", "unicode": "1F427", @@ -5964,11 +8069,21 @@ "unicode": "1F3F2", "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" }, + { + "name": "black_pennant", + "unicode": "1F3F2", + "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269" + }, { "name": "pennant_white", "unicode": "1F3F1", "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" }, + { + "name": "white_pennant", + "unicode": "1F3F1", + "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0" + }, { "name": "pensive", "unicode": "1F614", @@ -6109,11 +8224,21 @@ "unicode": "1F3D3", "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" }, + { + "name": "table_tennis", + "unicode": "1F3D3", + "digest": "dd2a84716c93410a285ff759bfbc2dc31a10f90b203c7a657b908e5949e89a39" + }, { "name": "piracy", "unicode": "1F572", "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" }, + { + "name": "no_piracy", + "unicode": "1F572", + "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9" + }, { "name": "pisces", "unicode": "2653", @@ -6129,6 +8254,11 @@ "unicode": "1F6D0", "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" }, + { + "name": "worship_symbol", + "unicode": "1F6D0", + "digest": "4fabc307b7e35f94288f6d53985485662a4814b11a9a382f0a3873d41b1290d3" + }, { "name": "play_pause", "unicode": "23EF", @@ -6299,6 +8429,21 @@ "unicode": "1F4A9", "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" }, + { + "name": "shit", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "hankey", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, + { + "name": "poo", + "unicode": "1F4A9", + "digest": "140ce75a015ede5e764873e0ae9a56e7b2af333eddca0fe2796b14545c620258" + }, { "name": "popcorn", "unicode": "1F37F", @@ -6419,11 +8564,21 @@ "unicode": "1F6C7", "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" }, + { + "name": "prohibited_sign", + "unicode": "1F6C7", + "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f" + }, { "name": "projector", "unicode": "1F4FD", "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" }, + { + "name": "film_projector", + "unicode": "1F4FD", + "digest": "fc361282f367926254c08150b02cb8fda7fa8d2c9c939d9360c78bf19a4f982e" + }, { "name": "punch", "unicode": "1F44A", @@ -6499,6 +8654,11 @@ "unicode": "1F3CE", "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" }, + { + "name": "racing_car", + "unicode": "1F3CE", + "digest": "2e9828e3884c79ad7e9e1173d3470790f3f56cfa08ef4e38deff45db0728c66c" + }, { "name": "racehorse", "unicode": "1F40E", @@ -6519,6 +8679,11 @@ "unicode": "2622", "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" }, + { + "name": "radioactive_sign", + "unicode": "2622", + "digest": "5ad8e8594617c0153672a76421deb836e05c6098020c33af3f975f8fcfe216e4" + }, { "name": "rage", "unicode": "1F621", @@ -6534,6 +8699,11 @@ "unicode": "1F6E4", "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" }, + { + "name": "railroad_track", + "unicode": "1F6E4", + "digest": "63ee881cc775d5b2711082b6c96ab44d5204c5d390afd6d8ee97e52aeeaa5e5e" + }, { "name": "rainbow", "unicode": "1F308", @@ -6744,11 +8914,21 @@ "unicode": "1F569", "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" }, + { + "name": "right_speaker_with_one_sound_wave", + "unicode": "1F569", + "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375" + }, { "name": "right_speaker_three", "unicode": "1F56A", "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" }, + { + "name": "right_speaker_with_three_sound_waves", + "unicode": "1F56A", + "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80" + }, { "name": "ring", "unicode": "1F48D", @@ -6764,6 +8944,11 @@ "unicode": "1F916", "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" }, + { + "name": "robot_face", + "unicode": "1F916", + "digest": "cc0e363774b86e21a5b2cea7f7af85bca9e92c124ebcd39c6067c125048baa60" + }, { "name": "rocket", "unicode": "1F680", @@ -6779,6 +8964,11 @@ "unicode": "1F644", "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" }, + { + "name": "face_with_rolling_eyes", + "unicode": "1F644", + "digest": "f596f203030b6c9bd743848512aa3fc7919447020d35ae5c2bf13ccb16fa2dbe" + }, { "name": "rooster", "unicode": "1F413", @@ -7099,11 +9289,21 @@ "unicode": "1F480", "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" }, + { + "name": "skeleton", + "unicode": "1F480", + "digest": "dfd169764b192ac7c6e5101277dd9f1e010e86bdd32ad37e00ed4499fc0a5dd6" + }, { "name": "skull_crossbones", "unicode": "2620", "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" }, + { + "name": "skull_and_crossbones", + "unicode": "2620", + "digest": "e2acf0f36b6a6800c1829a1c6551b5d0eb6dcdef4b7f02070cf69570aeab608c" + }, { "name": "sleeping", "unicode": "1F634", @@ -7124,11 +9324,21 @@ "unicode": "1F641", "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" }, + { + "name": "slightly_frowning_face", + "unicode": "1F641", + "digest": "3ae82b38b58ffa50eddebd87153428d880ca181f4f4178a9ca3bd813ea15ccbc" + }, { "name": "slight_smile", "unicode": "1F642", "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" }, + { + "name": "slightly_smiling_face", + "unicode": "1F642", + "digest": "5eee09f634a4e2031927d008a6530a258a00e611ead0c386dd5b7ebb5e75a306" + }, { "name": "slot_machine", "unicode": "1F3B0", @@ -7299,6 +9509,11 @@ "unicode": "1F5E3", "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" }, + { + "name": "speaking_head_in_silhouette", + "unicode": "1F5E3", + "digest": "d92cfe1200887300b2f05f9576448a2f2a79d0accd51f323a65ce3db0aa5639b" + }, { "name": "speech_balloon", "unicode": "1F4AC", @@ -7309,21 +9524,41 @@ "unicode": "1F5E8", "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" }, + { + "name": "left_speech_bubble", + "unicode": "1F5E8", + "digest": "478b0b07460a9f54b7d0050f886da59fde5e428daa11e899fc31477fda1707ed" + }, { "name": "speech_right", "unicode": "1F5E9", "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" }, + { + "name": "right_speech_bubble", + "unicode": "1F5E9", + "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a" + }, { "name": "speech_three", "unicode": "1F5EB", "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" }, + { + "name": "three_speech_bubbles", + "unicode": "1F5EB", + "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9" + }, { "name": "speech_two", "unicode": "1F5EA", "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" }, + { + "name": "two_speech_bubbles", + "unicode": "1F5EA", + "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983" + }, { "name": "speedboat", "unicode": "1F6A4", @@ -7344,31 +9579,61 @@ "unicode": "1F575", "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" }, + { + "name": "sleuth_or_spy", + "unicode": "1F575", + "digest": "eaa570a36d83119d0a596228e74affe84d7355714ff6901d88a89410d26dec2a" + }, { "name": "spy_tone1", "unicode": "1F575-1F3FB", "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" }, + { + "name": "sleuth_or_spy_tone1", + "unicode": "1F575-1F3FB", + "digest": "abdc066d4cad6a17047faf7806c45feb43ae1e2056cf500536f08f4173dbfa94" + }, { "name": "spy_tone2", "unicode": "1F575-1F3FC", "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" }, + { + "name": "sleuth_or_spy_tone2", + "unicode": "1F575-1F3FC", + "digest": "72a3313ef12364105e764cc3deabd47eb6bd086f261c435682ae1cd29dc8230b" + }, { "name": "spy_tone3", "unicode": "1F575-1F3FD", "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" }, + { + "name": "sleuth_or_spy_tone3", + "unicode": "1F575-1F3FD", + "digest": "2a1108d3d2e778f88aa5b3ae36705c877b84d0bf6b421409582ba748aeb2aee7" + }, { "name": "spy_tone4", "unicode": "1F575-1F3FE", "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" }, + { + "name": "sleuth_or_spy_tone4", + "unicode": "1F575-1F3FE", + "digest": "1d4fe62912384bc0d687bcf4565752caf0ed6146c903a156d1c6ba6ea239b154" + }, { "name": "spy_tone5", "unicode": "1F575-1F3FF", "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" }, + { + "name": "sleuth_or_spy_tone5", + "unicode": "1F575-1F3FF", + "digest": "69c1baac73783edb9e2d0c951f922dc7dddac34d0a9c818fee8d1021bc17db0d" + }, { "name": "stadium", "unicode": "1F3DF", @@ -7419,6 +9684,11 @@ "unicode": "1F4FE", "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" }, + { + "name": "portable_stereo", + "unicode": "1F4FE", + "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5" + }, { "name": "stew", "unicode": "1F372", @@ -7644,6 +9914,11 @@ "unicode": "1F57F", "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" }, + { + "name": "black_touchtone_telephone", + "unicode": "1F57F", + "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db" + }, { "name": "telephone_receiver", "unicode": "1F4DE", @@ -7654,6 +9929,11 @@ "unicode": "1F57E", "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" }, + { + "name": "white_touchtone_telephone", + "unicode": "1F57E", + "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581" + }, { "name": "telescope", "unicode": "1F52D", @@ -7684,11 +9964,21 @@ "unicode": "1F912", "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" }, + { + "name": "face_with_thermometer", + "unicode": "1F912", + "digest": "f19c489d89dd2d39770a6c8725a20f3e98f9e5216774af60c0665fd6a03a7687" + }, { "name": "thinking", "unicode": "1F914", "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" }, + { + "name": "thinking_face", + "unicode": "1F914", + "digest": "f64a9a18dca4c502b46f933838753a818b604a9d0268aa32eda26cbd31abc58c" + }, { "name": "thought_balloon", "unicode": "1F4AD", @@ -7699,11 +9989,21 @@ "unicode": "1F5EC", "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" }, + { + "name": "left_thought_bubble", + "unicode": "1F5EC", + "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46" + }, { "name": "thought_right", "unicode": "1F5ED", "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" }, + { + "name": "right_thought_bubble", + "unicode": "1F5ED", + "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae" + }, { "name": "three", "unicode": "0033-20E3", @@ -7714,76 +10014,151 @@ "unicode": "1F593", "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" }, + { + "name": "reversed_thumbs_down_sign", + "unicode": "1F593", + "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958" + }, { "name": "thumbs_up_reverse", "unicode": "1F592", "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" }, + { + "name": "reversed_thumbs_up_sign", + "unicode": "1F592", + "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837" + }, { "name": "thumbsdown", "unicode": "1F44E", "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" }, + { + "name": "-1", + "unicode": "1F44E", + "digest": "a98f742c9773e0d95c0de5e1c10d1ab373fa761378a205f27d095e85debe69a3" + }, { "name": "thumbsdown_tone1", "unicode": "1F44E-1F3FB", "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" }, + { + "name": "-1_tone1", + "unicode": "1F44E-1F3FB", + "digest": "5d0a7c63d52eafe6267c552168c5557a66622009d565c3cf7b5378c1f6e84bce" + }, { "name": "thumbsdown_tone2", "unicode": "1F44E-1F3FC", "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" }, + { + "name": "-1_tone2", + "unicode": "1F44E-1F3FC", + "digest": "ca5c15dc516660b2989a1c717bf3745fdfb6964c7acf3b938285ff6c7caf2ca2" + }, { "name": "thumbsdown_tone3", "unicode": "1F44E-1F3FD", "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" }, + { + "name": "-1_tone3", + "unicode": "1F44E-1F3FD", + "digest": "05740e3568795270674dac9134198bf75b1b778c11daa71649c88c231859ec16" + }, { "name": "thumbsdown_tone4", "unicode": "1F44E-1F3FE", "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" }, + { + "name": "-1_tone4", + "unicode": "1F44E-1F3FE", + "digest": "5ee93bcc2f515806462a7b303064beade2b22a3f43a8162e39fd65d15d772e27" + }, { "name": "thumbsdown_tone5", "unicode": "1F44E-1F3FF", "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" }, + { + "name": "-1_tone5", + "unicode": "1F44E-1F3FF", + "digest": "5c9ef8d53cf6f755668ab6dabfbfcdfd4b95fd59db3b3dd60290efefe9c33994" + }, { "name": "thumbsup", "unicode": "1F44D", "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" }, + { + "name": "+1", + "unicode": "1F44D", + "digest": "28b31df963773ba42a1a089f43cd89d0ce1ab0981e5410f41242e9a125fc1aee" + }, { "name": "thumbsup_tone1", "unicode": "1F44D-1F3FB", "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" }, + { + "name": "+1_tone1", + "unicode": "1F44D-1F3FB", + "digest": "f6365942738d2128b6959d6672b3d295757dc8240703cb84a2b014ad78d67de3" + }, { "name": "thumbsup_tone2", "unicode": "1F44D-1F3FC", "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" }, + { + "name": "+1_tone2", + "unicode": "1F44D-1F3FC", + "digest": "771d30146e4dc947a69057b05d32c765c8457ab02b5342889c5489acf27ef356" + }, { "name": "thumbsup_tone3", "unicode": "1F44D-1F3FD", "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" }, + { + "name": "+1_tone3", + "unicode": "1F44D-1F3FD", + "digest": "0bb7bbfb654c6139260e1786e7ffa5a33f31e19410c1d4d15737fdf5dd4c721d" + }, { "name": "thumbsup_tone4", "unicode": "1F44D-1F3FE", "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" }, + { + "name": "+1_tone4", + "unicode": "1F44D-1F3FE", + "digest": "df0927c5342f0075fbf4ea83b724e6f70c0466c54769c9ce4a5c2deb602b28aa" + }, { "name": "thumbsup_tone5", "unicode": "1F44D-1F3FF", "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" }, + { + "name": "+1_tone5", + "unicode": "1F44D-1F3FF", + "digest": "0683ae08c50aaf186c6406680a60617679c7b4bccd0817f24b15911dbb06866f" + }, { "name": "thunder_cloud_rain", "unicode": "26C8", "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" }, + { + "name": "thunder_cloud_and_rain", + "unicode": "26C8", + "digest": "dd836f06b41a10d6ed9bcbdae291d2886847ff66dc3ede2427382e469f60674c" + }, { "name": "ticket", "unicode": "1F3AB", @@ -7794,6 +10169,11 @@ "unicode": "1F39F", "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" }, + { + "name": "admission_tickets", + "unicode": "1F39F", + "digest": "ccafcc9583a84e847ff1eaa3d53187c5ab150a7d27c6a19363e59b9bc046b567" + }, { "name": "tiger", "unicode": "1F42F", @@ -7809,6 +10189,11 @@ "unicode": "23F2", "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" }, + { + "name": "timer_clock", + "unicode": "23F2", + "digest": "c48199312ed42ff53a33bb2791db19e2e2521223cd49d8f758ea95b9b379c5ff" + }, { "name": "tired_face", "unicode": "1F62B", @@ -7869,6 +10254,11 @@ "unicode": "1F6E0", "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" }, + { + "name": "hammer_and_wrench", + "unicode": "1F6E0", + "digest": "9b0a36dfdb475621d326359662b22cbdb80563c4f476aa5e7d7c00cdba605bd9" + }, { "name": "top", "unicode": "1F51D", @@ -7884,11 +10274,21 @@ "unicode": "23ED", "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" }, + { + "name": "next_track", + "unicode": "23ED", + "digest": "d5415ed140933f345fea8023a3d8fca30dcfcf7d19d9dc9771fa2cae9df62a3b" + }, { "name": "track_previous", "unicode": "23EE", "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" }, + { + "name": "previous_track", + "unicode": "23EE", + "digest": "97ff4a59a236e5cf506fa3577b20715b3b0197e0f343a50615b36185d5b835f1" + }, { "name": "trackball", "unicode": "1F5B2", @@ -7919,6 +10319,11 @@ "unicode": "1F6F2", "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" }, + { + "name": "diesel_locomotive", + "unicode": "1F6F2", + "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c" + }, { "name": "tram", "unicode": "1F68A", @@ -7929,6 +10334,11 @@ "unicode": "1F6C6", "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" }, + { + "name": "triangle_with_rounded_corners", + "unicode": "1F6C6", + "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5" + }, { "name": "triangular_flag_on_post", "unicode": "1F6A9", @@ -7994,6 +10404,11 @@ "unicode": "1F58F", "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" }, + { + "name": "turned_ok_hand_sign", + "unicode": "1F58F", + "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246" + }, { "name": "turtle", "unicode": "1F422", @@ -8109,6 +10524,11 @@ "unicode": "1F984", "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" }, + { + "name": "unicorn_face", + "unicode": "1F984", + "digest": "1b1e9c209dabe619db76fd346c3fb51b28ace0e4102697fe0973fe2d46aa9f08" + }, { "name": "unlock", "unicode": "1F513", @@ -8124,11 +10544,21 @@ "unicode": "1F643", "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" }, + { + "name": "upside_down_face", + "unicode": "1F643", + "digest": "763fe2baf07a9b04f96958adf38a43c7dd2bc70d57398f49604307bd835cbb53" + }, { "name": "urn", "unicode": "26B1", "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" }, + { + "name": "funeral_urn", + "unicode": "26B1", + "digest": "dbfd5b90709d1b812d2fff71a5cfa10f84a4579866c2d7cd0e80759a22b2ba0e" + }, { "name": "v", "unicode": "270C", @@ -8214,31 +10644,61 @@ "unicode": "1F596", "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers", + "unicode": "1F596", + "digest": "ca800fce797e652c5f47bf44992e8fbe19554688a36423fdf7c29ca6defae1e0" + }, { "name": "vulcan_tone1", "unicode": "1F596-1F3FB", "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1", + "unicode": "1F596-1F3FB", + "digest": "84bafdaca43426b053f5caa4e868ca109d99113a28ea9799db09d3c5d5f645c8" + }, { "name": "vulcan_tone2", "unicode": "1F596-1F3FC", "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2", + "unicode": "1F596-1F3FC", + "digest": "e7cedf63ead957ee5c287e4cb0828ba70673e17b604f92b529875c32d094e7e3" + }, { "name": "vulcan_tone3", "unicode": "1F596-1F3FD", "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3", + "unicode": "1F596-1F3FD", + "digest": "e124fef20f289921553274cf834f6dcc1a012889d30d9874dc5ad01afb8235b8" + }, { "name": "vulcan_tone4", "unicode": "1F596-1F3FE", "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4", + "unicode": "1F596-1F3FE", + "digest": "ea2115f549e4680467521bbf362b229f4a8f0fdadbfaf231378d801f9b369f08" + }, { "name": "vulcan_tone5", "unicode": "1F596-1F3FF", "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" }, + { + "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5", + "unicode": "1F596-1F3FF", + "digest": "1b322e1252491f35ae02f0b279b6529dad867f2a6b3c2c3e77f981bed07e447d" + }, { "name": "walking", "unicode": "1F6B6", @@ -8429,16 +10889,31 @@ "unicode": "1F325", "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" }, + { + "name": "white_sun_behind_cloud", + "unicode": "1F325", + "digest": "d8ce416e6bdb0e59e06e2fceac3177dbe59fefc248fd8c6d76b80d1418141070" + }, { "name": "white_sun_rain_cloud", "unicode": "1F326", "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" }, + { + "name": "white_sun_behind_cloud_with_rain", + "unicode": "1F326", + "digest": "d2b132518261864ac4a95707eaeea335dd8351ed2b8ef4e2272ced456e309bf1" + }, { "name": "white_sun_small_cloud", "unicode": "1F324", "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" }, + { + "name": "white_sun_with_small_cloud", + "unicode": "1F324", + "digest": "b86a72f1cdb4d24fd3ab180aae9db012ca51fc01f3786aab596c2e330066b185" + }, { "name": "wind_blowing_face", "unicode": "1F32C", @@ -8524,6 +10999,11 @@ "unicode": "1F58E", "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" }, + { + "name": "left_writing_hand", + "unicode": "1F58E", + "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb" + }, { "name": "writing_hand_tone1", "unicode": "270D-1F3FB", @@ -8589,6 +11069,11 @@ "unicode": "1F910", "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" }, + { + "name": "zipper_mouth_face", + "unicode": "1F910", + "digest": "8396249161b6d865861b56aabd17cae2c821b0d814f4249bf8cab0bb21fa8ee9" + }, { "name": "zzz", "unicode": "1F4A4", diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4c49442bf8..60b9f5e0ec 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -170,6 +170,10 @@ module API expose :label_names, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic + + expose :subscribed do |issue, options| + issue.subscribed?(options[:current_user]) + end end class MergeRequest < ProjectEntity @@ -183,6 +187,10 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status + + expose :subscribed do |merge_request, options| + merge_request.subscribed?(options[:current_user]) + end end class MergeRequestChanges < MergeRequest @@ -204,7 +212,7 @@ module API expose :note, as: :body expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic - expose :created_at + expose :created_at, :updated_at expose :system?, as: :system expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false @@ -255,14 +263,19 @@ module API expose :id, :path, :kind end - class ProjectAccess < Grape::Entity + class Member < Grape::Entity expose :access_level - expose :notification_level + expose :notification_level do |member, options| + if member.notification_setting + NotificationSetting.levels[member.notification_setting.level] + end + end end - class GroupAccess < Grape::Entity - expose :access_level - expose :notification_level + class ProjectAccess < Member + end + + class GroupAccess < Member end class ProjectService < Grape::Entity @@ -293,6 +306,7 @@ module API class Label < Grape::Entity expose :name, :color, :description + expose :open_issues_count, :closed_issues_count, :open_merge_requests_count end class Compare < Grape::Entity diff --git a/lib/api/groups.rb b/lib/api/groups.rb index c165de21a7..91e420832f 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -23,8 +23,10 @@ module API # Create group. Available only for users who can create groups. # # Parameters: - # name (required) - The name of the group - # path (required) - The path of the group + # name (required) - The name of the group + # path (required) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group # Example Request: # POST /groups post do @@ -42,6 +44,28 @@ module API end end + # Update group. Available only for users who can administrate groups. + # + # Parameters: + # id (required) - The ID of a group + # path (optional) - The path of the group + # description (optional) - The description of the group + # visibility_level (optional) - The visibility level of the group + # Example Request: + # PUT /groups/:id + put ':id' do + group = find_group(params[:id]) + authorize! :admin_group, group + + attrs = attributes_for_keys [:name, :path, :description, :visibility_level] + + if ::Groups::UpdateService.new(group, current_user, attrs).execute + present group, with: Entities::GroupDetail + else + render_validation_error!(group) + end + end + # Get a single group, with containing projects # # Parameters: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 4921ae99e7..5bbf721321 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -91,8 +91,7 @@ module API if can?(current_user, :read_group, group) group else - forbidden!("#{current_user.username} lacks sufficient "\ - "access to #{group.name}") + not_found!('Group') end end @@ -241,6 +240,10 @@ module API render_api_error!('413 Request Entity Too Large', 413) end + def not_modified! + render_api_error!('304 Not Modified', 304) + end + def render_validation_error!(model) if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1fee1dee1a..8aa08fd5ac 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -55,7 +55,7 @@ module API issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues.reorder(issuable_order_by => issuable_sort) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -92,7 +92,7 @@ module API end issues.reorder(issuable_order_by => issuable_sort) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user end # Get a single project issue @@ -105,7 +105,7 @@ module API get ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) not_found! unless can?(current_user, :read_issue, @issue) - present @issue, with: Entities::Issue + present @issue, with: Entities::Issue, current_user: current_user end # Create a new project issue @@ -117,7 +117,7 @@ module API # 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 - # created_at (optional) - The date + # created_at (optional) - Date time string, ISO 8601 formatted # Example Request: # POST /projects/:id/issues post ":id/issues" do @@ -149,7 +149,7 @@ module API issue.add_labels_by_names(params[:labels].split(',')) end - present issue, with: Entities::Issue + present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end @@ -166,12 +166,15 @@ module API # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue # state_event (optional) - The state event of an issue (close|reopen) + # updated_at (optional) - Date time string, ISO 8601 formatted # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do issue = user_project.issues.find(params[:issue_id]) authorize! :update_issue, issue - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] + keys = [:title, :description, :assignee_id, :milestone_id, :state_event] + keys << :updated_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? @@ -189,12 +192,35 @@ module API issue.add_labels_by_names(params[:labels].split(',')) end - present issue, with: Entities::Issue + present issue, with: Entities::Issue, current_user: current_user else render_validation_error!(issue) end end + # Move an existing issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # to_project_id (required) - The ID of the new project + # Example Request: + # POST /projects/:id/issues/:issue_id/move + post ':id/issues/:issue_id/move' do + required_attributes! [:to_project_id] + + issue = user_project.issues.find(params[:issue_id]) + new_project = Project.find(params[:to_project_id]) + + begin + issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) + present issue, with: Entities::Issue, current_user: current_user + rescue ::Issues::MoveService::MoveError => error + render_api_error!(error.message, 400) + end + end + + # # Delete a project issue # # Parameters: @@ -208,6 +234,42 @@ module API authorize!(:destroy_issue, issue) issue.destroy end + + # Subscribes to a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # POST /projects/:id/issues/:issue_id/subscription + post ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) + + if issue.subscribed?(current_user) + not_modified! + else + issue.toggle_subscription(current_user) + present issue, with: Entities::Issue, current_user: current_user + end + end + + # Unsubscribes from a project issue + # + # Parameters: + # id (required) - The ID of a project + # issue_id (required) - The ID of a project issue + # Example Request: + # DELETE /projects/:id/issues/:issue_id/subscription + delete ':id/issues/:issue_id/subscription' do + issue = user_project.issues.find(params[:issue_id]) + + if issue.subscribed?(current_user) + issue.unsubscribe(current_user) + present issue, with: Entities::Issue, current_user: current_user + else + not_modified! + end + end end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 93052fba06..7e78609ecb 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -56,7 +56,7 @@ module API end merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort) - present paginate(merge_requests), with: Entities::MergeRequest + present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user end # Create MR @@ -94,7 +94,7 @@ module API merge_request.add_labels_by_names(params[:labels].split(",")) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors end @@ -130,7 +130,7 @@ module API authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user end # Show MR commits @@ -162,7 +162,7 @@ module API merge_request = user_project.merge_requests. find(params[:merge_request_id]) authorize! :read_merge_request, merge_request - present merge_request, with: Entities::MergeRequestChanges + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end # Update MR @@ -204,7 +204,7 @@ module API merge_request.add_labels_by_names(params[:labels].split(",")) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user else handle_merge_request_errors! merge_request.errors end @@ -246,7 +246,7 @@ module API execute(merge_request) end - present merge_request, with: Entities::MergeRequest + present merge_request, with: Entities::MergeRequest, current_user: current_user end # Cancel Merge if Merge When build succeeds is enabled @@ -325,7 +325,43 @@ module API get "#{path}/closes_issues" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: Entities::Issue + present paginate(issues), with: Entities::Issue, current_user: current_user + end + + # Subscribes to a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # POST /projects/:id/issues/:merge_request_id/subscription + post "#{path}/subscription" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if merge_request.subscribed?(current_user) + not_modified! + else + merge_request.toggle_subscription(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + end + end + + # Unsubscribes from a merge request + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - The ID of a merge request + # Example Request: + # DELETE /projects/:id/merge_requests/:merge_request_id/subscription + delete "#{path}/subscription" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + if merge_request.subscribed?(current_user) + merge_request.unsubscribe(current_user) + present merge_request, with: Entities::MergeRequest, current_user: current_user + else + not_modified! + end end end end diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index afb6ffa360..84b4d4cdd6 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -21,6 +21,7 @@ module API # state (optional) - Return "active" or "closed" milestones # Example Request: # GET /projects/:id/milestones + # GET /projects/:id/milestones?iid=42 # GET /projects/:id/milestones?state=active # GET /projects/:id/milestones?state=closed get ":id/milestones" do @@ -28,6 +29,7 @@ module API milestones = user_project.milestones milestones = filter_milestones_state(milestones, params[:state]) + milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? present paginate(milestones), with: Entities::Milestone end @@ -103,7 +105,7 @@ module API authorize! :read_milestone, user_project @milestone = user_project.milestones.find(params[:milestone_id]) - present paginate(@milestone.issues), with: Entities::Issue + present paginate(@milestone.issues), with: Entities::Issue, current_user: current_user end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 174473f537..71a53e6f0d 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -61,6 +61,7 @@ module API # id (required) - The ID of a project # noteable_id (required) - The ID of an issue or snippet # body (required) - The content of a note + # created_at (optional) - The date # Example Request: # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes @@ -73,6 +74,10 @@ module API noteable_id: params[noteable_id_str] } + if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) + opts[:created_at] = params[:created_at] + end + @note = ::Notes::CreateService.new(user_project, current_user, opts).execute if @note.valid? @@ -112,6 +117,23 @@ module API end end + # Delete a +noteable+ note + # + # Parameters: + # id (required) - The ID of a project + # noteable_id (required) - The ID of an issue, MR, or snippet + # node_id (required) - The ID of a note + # Example Request: + # DELETE /projects/:id/issues/:noteable_id/notes/:note_id + # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id + delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + note = user_project.notes.find(params[:note_id]) + authorize! :admin_note, note + + ::Notes::DeleteService.new(user_project, current_user).execute(note) + + present note, with: Entities::Note + end end end end diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index c756bb479f..4aefdf319c 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -93,12 +93,17 @@ module API # Example Request: # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do - authorize! :admin_project, user_project project_member = user_project.project_members.find_by(user_id: params[:user_id]) - unless project_member.nil? - project_member.destroy - else + + unless current_user.can?(:admin_project, user_project) || + current_user.can?(:destroy_project_member, project_member) + forbidden! + end + + if project_member.nil? { message: "Access revoked", id: params[:user_id].to_i } + else + project_member.destroy end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 24b3100547..cc2c7a0c50 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -272,6 +272,40 @@ module API present user_project, with: Entities::Project end + # Star project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # POST /projects/:id/star + post ':id/star' do + if current_user.starred?(user_project) + not_modified! + else + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + end + end + + # Unstar project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # DELETE /projects/:id/star + delete ':id/star' do + if current_user.starred?(user_project) + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + else + not_modified! + end + end + # Remove project # # Parameters: diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 0d0f0d4616..62161aadb9 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -98,7 +98,6 @@ module API authorize! :download_code, user_project begin - RepositoryArchiveCacheWorker.perform_async header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) rescue not_found!('File') diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 2d8a9e51bb..d1a10479e4 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -16,6 +16,20 @@ module API with: Entities::RepoTag, project: user_project end + # Get a single repository tag + # + # Parameters: + # id (required) - The ID of a project + # tag_name (required) - The name of the tag + # Example Request: + # GET /projects/:id/repository/tags/:tag_name + get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do + tag = user_project.repository.find_tag(params[:tag_name]) + not_found!('Tag') unless tag + + present tag, with: Entities::RepoTag, project: user_project + end + # Create tag # # Parameters: diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ae714c87dc..c14a9c4c72 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -19,8 +19,10 @@ module Banzai cache_key = full_cache_key(cache_key, context[:pipeline]) if cache_key - Rails.cache.fetch(cache_key) do - cacheless_render(text, context) + Gitlab::Metrics.measure(:banzai_cached_render) do + Rails.cache.fetch(cache_key) do + cacheless_render(text, context) + end end else cacheless_render(text, context) @@ -64,13 +66,15 @@ module Banzai private def self.cacheless_render(text, context = {}) - result = render_result(text, context) + Gitlab::Metrics.measure(:banzai_cacheless_render) do + result = render_result(text, context) - output = result[:output] - if output.respond_to?(:to_html) - output.to_html - else - output.to_s + output = result[:output] + if output.respond_to?(:to_html) + output.to_html + else + output.to_s + end end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 6108697bc2..7479e729db 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,4 +1,7 @@ require 'gitlab/git' module Gitlab + def self.com? + Gitlab.config.gitlab.url == 'https://gitlab.com' + end end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index c73eca832d..ffe4936437 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -43,18 +43,15 @@ module Gitlab # false if the lease is already taken. def try_obtain # Performing a single SET is atomic - !!redis.set(redis_key, '1', nx: true, ex: @timeout) + Gitlab::Redis.with do |redis| + !!redis.set(redis_key, '1', nx: true, ex: @timeout) + end end # No #cancel method. See comments above! private - def redis - # Maybe someday we want to use a connection pool... - @redis ||= Redis.new(url: Gitlab::RedisConfig.url) - end - def redis_key "gitlab:exclusive_lease:#{@key}" end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb new file mode 100644 index 0000000000..5ebaad6ca6 --- /dev/null +++ b/lib/gitlab/gon_helper.rb @@ -0,0 +1,17 @@ +module Gitlab + module GonHelper + def add_gon_variables + gon.api_version = API::API.version + gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s + gon.default_issues_tracker = Project.new.default_issue_tracker.to_param + gon.max_file_size = current_application_settings.max_attachment_size + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + + if current_user + gon.current_user_id = current_user.id + gon.api_token = current_user.private_token + end + end + end +end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 4a3f47b5a9..484970c5a1 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -74,28 +74,46 @@ module Gitlab # # Example: # - # Gitlab::Metrics.measure(:find_by_username_timings) do + # Gitlab::Metrics.measure(:find_by_username_duration) do # User.find_by_username(some_username) # end # - # series - The name of the series to store the data in. - # values - A Hash containing extra values to add to the metric. - # tags - A Hash containing extra tags to add to the metric. + # name - The name of the field to store the execution time in. # # Returns the value yielded by the supplied block. - def self.measure(series, values = {}, tags = {}) - return yield unless Transaction.current + def self.measure(name) + trans = current_transaction + + return yield unless trans + + real_start = Time.now.to_f + cpu_start = System.cpu_time - start = Time.now.to_f retval = yield - duration = (Time.now.to_f - start) * 1000.0 - values = values.merge(duration: duration) - Transaction.current.add_metric(series, values, tags) + cpu_stop = System.cpu_time + real_stop = Time.now.to_f + + real_time = (real_stop - real_start) * 1000.0 + cpu_time = cpu_stop - cpu_start + + trans.increment("#{name}_real_time", real_time) + trans.increment("#{name}_cpu_time", cpu_time) + trans.increment("#{name}_call_count", 1) retval end + # Adds a tag to the current transaction (if any) + # + # name - The name of the tag to add. + # value - The value of the tag. + def self.tag_transaction(name, value) + trans = current_transaction + + trans.add_tag(name, value) if trans + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? @@ -107,5 +125,11 @@ module Gitlab new(udp: { host: host, port: port }) end end + + private + + def self.current_transaction + Transaction.current + end end end diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb index 7ea9555cc8..1cd1ca30f7 100644 --- a/lib/gitlab/metrics/metric.rb +++ b/lib/gitlab/metrics/metric.rb @@ -2,6 +2,8 @@ module Gitlab module Metrics # Class for storing details of a single metric (label, value, etc). class Metric + JITTER_RANGE = 0.000001..0.001 + attr_reader :series, :values, :tags, :created_at # series - The name of the series (as a String) to store the metric in. @@ -16,11 +18,29 @@ module Gitlab # Returns a Hash in a format that can be directly written to InfluxDB. def to_hash + # InfluxDB overwrites an existing point if a new point has the same + # series, tag set, and timestamp. In a highly concurrent environment + # this means that using the number of seconds since the Unix epoch is + # inevitably going to collide with another timestamp. For example, two + # Rails requests processed by different processes may end up generating + # metrics using the _exact_ same timestamp (in seconds). + # + # Due to the way InfluxDB is set up there's no solution to this problem, + # all we can do is lower the amount of collisions. We do this by using + # Time#to_f which returns the seconds as a Float providing greater + # accuracy. We then add a small random value that is large enough to + # distinguish most timestamps but small enough to not alter the amount + # of seconds. + # + # See https://gitlab.com/gitlab-com/operations/issues/175 for more + # information. + time = @created_at.to_f + rand(JITTER_RANGE) + { series: @series, tags: @tags, values: @values, - timestamp: @created_at.to_i * 1_000_000_000 + timestamp: (time * 1_000_000_000).to_i } end end diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb new file mode 100644 index 0000000000..49e5f86e6e --- /dev/null +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -0,0 +1,39 @@ +module Gitlab + module Metrics + module Subscribers + # Class for tracking the total time spent in Rails cache calls + class RailsCache < ActiveSupport::Subscriber + attach_to :active_support + + def cache_read(event) + increment(:cache_read_duration, event.duration) + end + + def cache_write(event) + increment(:cache_write_duration, event.duration) + end + + def cache_delete(event) + increment(:cache_delete_duration, event.duration) + end + + def cache_exist?(event) + increment(:cache_exists_duration, event.duration) + end + + def increment(key, duration) + return unless current_transaction + + current_transaction.increment(:cache_duration, duration) + current_transaction.increment(key, duration) + end + + private + + def current_transaction + Transaction.current + end + end + end + end +end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index 8337126527..a7d183b2f9 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -30,6 +30,17 @@ module Gitlab 0 end end + + # THREAD_CPUTIME is not supported on OS X + if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) + def self.cpu_time + Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) + end + else + def self.cpu_time + Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) + end + end end end end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 18523e0aef..8bdc89a775 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -59,8 +59,7 @@ module Gitlab repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } - base_data[:object_attributes][:url] = - Gitlab::UrlBuilder.new(:note).build(note.id) + base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note) base_data end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 832fb08a52..356e96fcba 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -54,6 +54,12 @@ module Gitlab @user ||= build_new_user end + if external_provider? && @user + @user.external = true + elsif @user + @user.external = false + end + @user end @@ -113,6 +119,10 @@ module Gitlab end end + def external_provider? + Gitlab.config.omniauth.external_providers.include?(auth_hash.provider) + end + def block_after_signup? if creating_linked_ldap_user? ldap_config.block_auto_created_users diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb new file mode 100644 index 0000000000..5c352c96de --- /dev/null +++ b/lib/gitlab/redis.rb @@ -0,0 +1,50 @@ +module Gitlab + class Redis + CACHE_NAMESPACE = 'cache:gitlab' + SESSION_NAMESPACE = 'session:gitlab' + SIDEKIQ_NAMESPACE = 'resque:gitlab' + + attr_reader :url + + # To be thread-safe we must be careful when writing the class instance + # variables @url and @pool. Because @pool depends on @url we need two + # mutexes to prevent deadlock. + URL_MUTEX = Mutex.new + POOL_MUTEX = Mutex.new + private_constant :URL_MUTEX, :POOL_MUTEX + + def self.url + @url || URL_MUTEX.synchronize { @url = new.url } + end + + def self.with + if @pool.nil? + POOL_MUTEX.synchronize do + @pool = ConnectionPool.new { ::Redis.new(url: url) } + end + end + @pool.with { |redis| yield redis } + end + + def self.redis_store_options + url = new.url + redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) + # Redis::Store does not handle Unix sockets well, so let's do it for them + redis_uri = URI.parse(url) + if redis_uri.scheme == 'unix' + redis_config_hash[:path] = redis_uri.path + end + redis_config_hash + end + + def initialize(rails_env=nil) + rails_env ||= Rails.env + config_file = File.expand_path('../../../config/resque.yml', __FILE__) + + @url = "redis://localhost:6379" + if File.exists?(config_file) + @url =YAML.load_file(config_file)[rails_env] + end + end + end +end diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb deleted file mode 100644 index 4949c6db53..0000000000 --- a/lib/gitlab/redis_config.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Gitlab - class RedisConfig - attr_reader :url - - def self.url - new.url - end - - def self.redis_store_options - url = new.url - redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url) - # Redis::Store does not handle Unix sockets well, so let's do it for them - redis_uri = URI.parse(url) - if redis_uri.scheme == 'unix' - redis_config_hash[:path] = redis_uri.path - end - redis_config_hash - end - - def initialize(rails_env=nil) - rails_env ||= Rails.env - config_file = File.expand_path('../../../config/resque.yml', __FILE__) - - @url = "redis://localhost:6379" - if File.exists?(config_file) - @url =YAML.load_file(config_file)[rails_env] - end - end - end -end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index c1072452ab..dba4bbfc89 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -26,7 +26,7 @@ module Gitlab @user ||= build_new_user end - if external_users_enabled? + if external_users_enabled? && @user # Check if there is overlap between the user's groups and the external groups # setting then set user as external or internal. if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? @@ -48,6 +48,7 @@ module Gitlab end def changed? + return true unless gl_user gl_user.changed? || gl_user.identities.any?(&:changed?) end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index f301d42939..f1943222ed 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -4,50 +4,58 @@ module Gitlab include GitlabRoutingHelper include ActionView::RecordIdentifier - def initialize(type) - @type = type + attr_reader :object + + def self.build(object) + new(object).url end - def build(id) - case @type - when :issue - build_issue_url(id) - when :merge_request - build_merge_request_url(id) - when :note - build_note_url(id) - + def url + case object + when Commit + commit_url + when Issue + issue_url(object) + when MergeRequest + merge_request_url(object) + when Note + note_url + else + raise NotImplementedError.new("No URL builder defined for #{object.class}") end end private - def build_issue_url(id) - issue = Issue.find(id) - issue_url(issue) + def initialize(object) + @object = object end - def build_merge_request_url(id) - merge_request = MergeRequest.find(id) - merge_request_url(merge_request) + def commit_url(opts = {}) + return '' if object.project.nil? + + namespace_project_commit_url({ + namespace_id: object.project.namespace, + project_id: object.project, + id: object.id + }.merge!(opts)) end - def build_note_url(id) - note = Note.find(id) - if note.for_commit? - namespace_project_commit_url(namespace_id: note.project.namespace, - id: note.commit_id, - project_id: note.project, - anchor: dom_id(note)) - elsif note.for_issue? - issue = Issue.find(note.noteable_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: dom_id(note)) - elsif note.for_snippet? - snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, anchor: dom_id(note)) + def note_url + if object.for_commit? + commit_url(id: object.commit_id, anchor: dom_id(object)) + + elsif object.for_issue? + issue = Issue.find(object.noteable_id) + issue_url(issue, anchor: dom_id(object)) + + elsif object.for_merge_request? + merge_request = MergeRequest.find(object.noteable_id) + merge_request_url(merge_request, anchor: dom_id(object)) + + elsif object.for_snippet? + snippet = Snippet.find(object.noteable_id) + project_snippet_url(snippet, anchor: dom_id(object)) end end end diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 51e746ef92..2214f85520 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -4,18 +4,19 @@ namespace :cache do desc "GitLab | Clear redis cache" task :clear => :environment do - redis = Redis.new(url: Gitlab::RedisConfig.url) - cursor = REDIS_SCAN_START_STOP - loop do - cursor, keys = redis.scan( - cursor, - match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", - count: CLEAR_BATCH_SIZE - ) - - redis.del(*keys) if keys.any? - - break if cursor == REDIS_SCAN_START_STOP + Gitlab::Redis.with do |redis| + cursor = REDIS_SCAN_START_STOP + loop do + cursor, keys = redis.scan( + cursor, + match: "#{Gitlab::Redis::CACHE_NAMESPACE}*", + count: CLEAR_BATCH_SIZE + ) + + redis.del(*keys) if keys.any? + + break if cursor == REDIS_SCAN_START_STOP + end end end end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 7ec00a898f..030ee8bafc 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -5,12 +5,23 @@ namespace :gemojione do require 'json' dir = Gemojione.index.images_path + digests = [] + aliases = Hash.new { |hash, key| hash[key] = [] } + aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') - digests = AwardEmoji.emojis.map do |name, emoji_hash| + JSON.parse(File.read(aliases_path)).each do |alias_name, real_name| + aliases[real_name] << alias_name + end + + 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 } + digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } + + aliases[name].each do |alias_name| + digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest } + end end out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 4cbccf2ca8..48baecfd2a 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -14,7 +14,7 @@ namespace :gitlab do puts "" end - Rake::Task["db:setup"].invoke + Rake::Task["db:reset"].invoke Rake::Task["add_limits_mysql"].invoke Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index eb0c6ac6d8..b0793cb165 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -23,5 +23,11 @@ describe Groups::MilestonesController do expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) expect(Milestone.where(title: title).count).to eq(2) end + + it "redirects to new when there are no project ids" do + post :create, group_id: group.id, milestone: { title: title, project_ids: [""] } + expect(response).to render_template :new + expect(assigns(:milestone).errors).not_to be_nil + end end end diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb new file mode 100644 index 0000000000..0786e45515 --- /dev/null +++ b/spec/controllers/groups/notification_settings_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Groups::NotificationSettingsController do + let(:group) { create(:group) } + let(:user) { create(:user) } + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + group_id: group.to_param, + notification_setting: { level: :participating } + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + group_id: group.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 75e6b6f45a..c54e83339a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -157,6 +157,34 @@ describe Projects::MergeRequestsController do end end + describe 'PUT #update' do + context 'there is no source project' do + let(:project) { create(:project) } + let(:fork_project) { create(:forked_project_with_submodules) } + let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + + before do + fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + fork_project.save + merge_request.reload + fork_project.destroy + end + + it 'closes MR without errors' do + post :update, + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + merge_request: { + state_event: 'close' + } + + expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) + expect(merge_request.reload.closed?).to be_truthy + end + end + end + describe "DELETE #destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb new file mode 100644 index 0000000000..4908b54564 --- /dev/null +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Projects::NotificationSettingsController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + project.team << [user, :developer] + end + + describe '#update' do + context 'when not authorized' do + it 'redirects to sign in page' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'when authorized' do + before do + sign_in(user) + end + + it 'returns success' do + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq 200 + end + end + end +end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb new file mode 100644 index 0000000000..ac6eb0a789 --- /dev/null +++ b/spec/factories/commits.rb @@ -0,0 +1,12 @@ +require_relative '../support/repo_helpers' + +FactoryGirl.define do + factory :commit do + git_commit RepoHelpers.sample_commit + project factory: :empty_project + + initialize_with do + new(git_commit, project) + end + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb new file mode 100644 index 0000000000..7700b15d53 --- /dev/null +++ b/spec/factories/oauth_access_tokens.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id :integer not null, primary key +# resource_owner_id :integer +# application_id :integer +# token :string not null +# refresh_token :string +# expires_in :integer +# revoked_at :datetime +# created_at :datetime not null +# scopes :string +# + +FactoryGirl.define do + factory :oauth_access_token do + resource_owner + application + token '123456' + end +end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb new file mode 100644 index 0000000000..d116a57383 --- /dev/null +++ b/spec/factories/oauth_applications.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do + name { FFaker::Name.name } + uid { FFaker::Name.name } + redirect_uri { FFaker::Internet.uri('http') } + owner + owner_type 'User' + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a5c60c51c5..a9b2148bd2 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,7 +1,7 @@ FactoryGirl.define do sequence(:name) { FFaker::Name.name } - factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do + factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do email { FFaker::Internet.email } name sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb new file mode 100644 index 0000000000..39805da9d0 --- /dev/null +++ b/spec/features/dashboard_issues_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe "Dashboard Issues filtering", feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:milestone) { create(:milestone, project: project) } + + context 'filtering by milestone' do + before do + project.team << [user, :master] + login_as(user) + + create(:issue, project: project, author: user, assignee: user) + create(:issue, project: project, author: user, assignee: user, milestone: milestone) + + visit_issues + end + + it 'should show all issues with no milestone' do + show_milestone_dropdown + + click_link 'No Milestone' + + expect(page).to have_selector('.issue', count: 1) + end + + it 'should show all issues with any milestone' do + show_milestone_dropdown + + click_link 'Any Milestone' + + expect(page).to have_selector('.issue', count: 2) + end + + it 'should show all issues with the selected milestone' do + show_milestone_dropdown + + page.within '.dropdown-content' do + click_link milestone.title + end + + expect(page).to have_selector('.issue', count: 1) + end + end + + def show_milestone_dropdown + click_button 'Milestone' + expect(page).to have_selector('.dropdown-content', visible: true) + end + + def visit_issues + visit issues_dashboard_path + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb new file mode 100644 index 0000000000..41af789aae --- /dev/null +++ b/spec/features/issues/award_emoji_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +describe 'Awards Emoji', feature: true do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as(user) + end + + describe 'Click award emoji from issue#show' do + let!(:issue) do + create(:issue, + author: @user, + assignee: @user, + project: project) + end + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should increment the thumbsdown emoji', js: true do + find('[data-emoji="thumbsdown"]').click + sleep 2 + expect(thumbsdown_emoji).to have_text("1") + end + + context 'click the thumbsup emoji' do + + it 'should increment the thumbsup emoji', js: true do + find('[data-emoji="thumbsup"]').click + sleep 2 + expect(thumbsup_emoji).to have_text("1") + end + + it 'should decrement the thumbsdown emoji', js: true do + expect(thumbsdown_emoji).to have_text("0") + end + end + + context 'click the thumbsdown emoji' do + + it 'should increment the thumbsdown emoji', js: true do + find('[data-emoji="thumbsdown"]').click + sleep 2 + expect(thumbsdown_emoji).to have_text("1") + end + + it 'should decrement the thumbsup emoji', js: true do + expect(thumbsup_emoji).to have_text("0") + end + end + end + + def thumbsup_emoji + page.all('span.js-counter').first + end + + def thumbsdown_emoji + page.all('span.js-counter').last + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 90822a8c12..69b22232f1 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -76,6 +76,37 @@ describe 'Filter issues', feature: true do end end + describe 'Filter issues for label from issues#index', js: true do + before do + visit namespace_project_issues_path(project.namespace, project) + find('.js-label-select').click + end + + it 'should filter by any label' do + find('.dropdown-menu-labels a', text: 'Any Label').click + page.within '.labels-filter' do + expect(page).to have_content 'Any Label' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: 'No Label').click + page.within '.labels-filter' do + expect(page).to have_content 'No Label' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') + end + + it 'should filter by no label' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content label.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + describe 'Filter issues for assignee and label from issues#index' do before do diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index fd02d58484..00b60bd0e7 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Create New Merge Request', feature: true, js: false do +feature 'Create New Merge Request', feature: true, js: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -13,9 +13,12 @@ feature 'Create New Merge Request', feature: true, js: false do 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" + + first('.js-source-branch').click + first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click + click_button "Compare branches" + click_link "Changes" expect(page).to have_content "README.md" expect(page).to have_content "wm.png" @@ -23,6 +26,8 @@ feature 'Create New Merge Request', feature: true, js: false do fill_in "merge_request_title", with: "Orphaned MR test" click_button "Submit merge request" + click_link "Check out branch" + expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' end end diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb new file mode 100644 index 0000000000..9e007ab763 --- /dev/null +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Edit Merge Request', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } + + before do + project.team << [user, :master] + + login_as user + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'editing a MR' do + it 'form should have class js-quick-submit' do + expect(page).to have_selector('.js-quick-submit') + end + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 70d0864783..5f855ccc70 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -210,7 +210,7 @@ describe 'Comments', feature: true do is_expected.to have_content('Another comment on line 10') is_expected.to have_css('.notes_holder') is_expected.to have_css('.notes_holder .note', count: 1) - is_expected.to have_button('Reply') + is_expected.to have_button('Reply...') end end end diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb new file mode 100644 index 0000000000..1a5a9059db --- /dev/null +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Profile > Applications', feature: true do + let(:user) { create(:user) } + + before do + login_as(user) + end + + describe 'User manages applications', js: true do + it 'deletes an application' do + create(:oauth_application, owner: user) + visit oauth_applications_path + + page.within('.oauth-applications') do + expect(page).to have_content('Your applications (1)') + click_button 'Destroy' + end + + expect(page).to have_content('The application was deleted successfully') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + + it 'deletes an authorized application' do + create(:oauth_access_token, resource_owner: user) + visit oauth_applications_path + + page.within('.oauth-authorized-applications') do + expect(page).to have_content('Authorized applications (1)') + click_button 'Revoke' + end + + expect(page).to have_content('The application was revoked access.') + expect(page).to have_content('Your applications (0)') + expect(page).to have_content('Authorized applications (0)') + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index ed97b6cb57..782c0bfe66 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -100,8 +100,7 @@ feature 'Project', feature: true do it 'click toggle and show dropdown', js: true do find('.js-projects-dropdown-toggle').click - wait_for_ajax - expect(page).to have_css('.select2-results li', count: 1) + expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 3e6289a46b..029a11ea43 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -10,6 +10,10 @@ describe "Search", feature: true do visit search_path end + it 'top right search form is not present' do + expect(page).not_to have_selector('.search') + end + describe 'searching for Projects' do it 'finds a project' do page.within '.search-holder' do diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb new file mode 100644 index 0000000000..b20373a96f --- /dev/null +++ b/spec/helpers/form_helper_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe FormHelper do + describe 'form_errors' do + it 'returns nil when model has no errors' do + model = double(errors: []) + + expect(helper.form_errors(model)).to be_nil + end + + it 'renders an alert div' do + model = double(errors: errors_stub('Error 1')) + + expect(helper.form_errors(model)). + to include('
') + end + + it 'contains a summary message' do + single_error = double(errors: errors_stub('A')) + multi_errors = double(errors: errors_stub('A', 'B', 'C')) + + expect(helper.form_errors(single_error)). + to include('

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

The form contains the following errors:') + end + + it 'renders each message' do + model = double(errors: errors_stub('Error 1', 'Error 2', 'Error 3')) + + errors = helper.form_errors(model) + + aggregate_failures do + expect(errors).to include('
  • Error 1
  • ') + expect(errors).to include('
  • Error 2
  • ') + expect(errors).to include('
  • Error 3
  • ') + end + end + + def errors_stub(*messages) + ActiveModel::Errors.new(double).tap do |errors| + messages.each { |msg| errors.add(:base, msg) } + end + end + end +end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9adcd916ce..13de88e2f2 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -150,13 +150,6 @@ describe GitlabMarkdownHelper do end end - describe 'random_markdown_tip' do - it 'returns a random Markdown tip' do - stub_const("#{described_class}::MARKDOWN_TIPS", ['Random tip']) - expect(random_markdown_tip).to eq 'Random tip' - end - end - describe '#first_line_in_markdown' do let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ffd8ebae02..543593cf38 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -80,7 +80,7 @@ describe IssuesHelper do end end - describe '#url_for_new_issue' do + describe 'url_for_new_issue' do let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } let(:ext_expected) do issues_url.gsub(':project_id', ext_project.id.to_s) @@ -117,7 +117,7 @@ describe IssuesHelper do end end - describe "#merge_requests_sentence" do + describe "merge_requests_sentence" do subject { merge_requests_sentence(merge_requests)} let(:merge_requests) do [ build(:merge_request, iid: 1), build(:merge_request, iid: 2), @@ -127,7 +127,7 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "#note_active_class" do + describe "note_active_class" do before do @note = create :note @note1 = create :note @@ -142,10 +142,25 @@ describe IssuesHelper do end end - describe "#awards_sort" do + describe "awards_sort" do it "sorts a hash so thumbsup and thumbsdown are always on top" do data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" } expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"]) end end + + describe "milestone_options" do + it "gets closed milestone from current issue" do + closed_milestone = create(:closed_milestone, project: project) + milestone1 = create(:milestone, project: project) + milestone2 = create(:milestone, project: project) + issue.update_attributes(milestone_id: closed_milestone.id) + + options = milestone_options(issue) + + expect(options).to have_selector('option[selected]', text: closed_milestone.title) + expect(options).to have_selector('option', text: milestone1.title) + expect(options).to have_selector('option', text: milestone2.title) + end + end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index f1aba4cfdf..9d5f009ebe 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -2,34 +2,15 @@ require 'spec_helper' describe NotificationsHelper do describe 'notification_icon' do - let(:notification) { double(disabled?: false, participating?: false, watch?: false) } + it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') } + it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') } + it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') } + it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') } + it { expect(notification_icon(:watch)).to match('class="fa fa-eye fa-fw"') } + end - context "disabled notification" do - before { allow(notification).to receive(:disabled?).and_return(true) } - - it "has a red icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"') - end - end - - context "participating notification" do - before { allow(notification).to receive(:participating?).and_return(true) } - - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"') - end - end - - context "watched notification" do - before { allow(notification).to receive(:watch?).and_return(true) } - - it "has a green icon" do - expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"') - end - end - - it "has a blue icon" do - expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"') - end + describe 'notification_title' do + it { expect(notification_title(:watch)).to match('Watch') } + it { expect(notification_title(:mention)).to match('On mention') } end end diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml index e5850b6265..4547feeb21 100644 --- a/spec/javascripts/fixtures/project_title.html.haml +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -1,7 +1,20 @@ -%h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"} - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle +.header-content + %h1.title + %a + GitLab Org + %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} + GitLab Test + %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content" } + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + .dropdown-title + %span Go to a project + %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} + %i.fa.fa-times.dropdown-menu-close-icon + .dropdown-input + %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} + %i.fa.fa-search.dropdown-input-search + %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} + .dropdown-content + .dropdown-loading + %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee index 050b6e362c..dd160e821b 100644 --- a/spec/javascripts/notes_spec.js.coffee +++ b/spec/javascripts/notes_spec.js.coffee @@ -1,4 +1,5 @@ #= require notes +#= require gl_form window.gon = {} window.disableButtonIfEmptyField = -> null diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 47c7b7febe..3d8de2ff98 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,4 +1,6 @@ +#= require bootstrap #= require select2 +#= require gl_dropdown #= require api #= require project_select #= require project @@ -14,9 +16,6 @@ describe 'Project Title', -> fixture.load('project_title.html') @project = new Project() - spyOn(@project, 'changeProject').and.callFake (url) -> - window.current_project_url = url - describe 'project list', -> beforeEach => @projects_data = fixture.load('projects.json')[0] @@ -29,18 +28,9 @@ describe 'Project Title', -> it 'to show on toggle click', => $('.js-projects-dropdown-toggle').click() - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true) - expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length) + expect($('.header-content').hasClass('open')).toBe(true) it 'hide dropdown', -> - $("#select2-drop-mask").click() + $(".dropdown-menu-close-icon").click() - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - - it 'change project when clicking item', -> - $('.js-projects-dropdown-toggle').click() - $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup') - - expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) - expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate') + expect($('.header-content').hasClass('open')).toBe(false) diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index a1f51429a7..e9b8ce6b5b 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -22,11 +22,21 @@ describe Gitlab::ClosingIssueExtractor, lib: true do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Closes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (closes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) @@ -37,106 +47,211 @@ describe Gitlab::ClosingIssueExtractor, lib: true do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "closed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Closing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "closing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Close: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "close: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (Fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Fixes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Fixes: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fixed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fixed: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fixing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fixing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Fix: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "fix: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (Resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (Resolves: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Awesome commit (resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Awesome commit (resolves: #{reference})" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolved: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolved: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolving: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolving: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "Resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Resolve: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + it do message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "resolve: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb new file mode 100644 index 0000000000..e01b0b4bd2 --- /dev/null +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::Metrics::Subscribers::RailsCache do + let(:transaction) { Gitlab::Metrics::Transaction.new } + let(:subscriber) { described_class.new } + + let(:event) { double(:event, duration: 15.2) } + + describe '#cache_read' do + it 'increments the cache_read duration' do + expect(subscriber).to receive(:increment). + with(:cache_read_duration, event.duration) + + subscriber.cache_read(event) + end + end + + describe '#cache_write' do + it 'increments the cache_write duration' do + expect(subscriber).to receive(:increment). + with(:cache_write_duration, event.duration) + + subscriber.cache_write(event) + end + end + + describe '#cache_delete' do + it 'increments the cache_delete duration' do + expect(subscriber).to receive(:increment). + with(:cache_delete_duration, event.duration) + + subscriber.cache_delete(event) + end + end + + describe '#cache_exist?' do + it 'increments the cache_exists duration' do + expect(subscriber).to receive(:increment). + with(:cache_exists_duration, event.duration) + + subscriber.cache_exist?(event) + end + end + + describe '#increment' do + context 'without a transaction' do + it 'returns' do + expect(transaction).not_to receive(:increment) + + subscriber.increment(:foo, 15.2) + end + end + + context 'with a transaction' do + before do + allow(subscriber).to receive(:current_transaction). + and_return(transaction) + end + + it 'increments the total and specific cache duration' do + expect(transaction).to receive(:increment). + with(:cache_duration, event.duration) + + expect(transaction).to receive(:increment). + with(:cache_delete_duration, event.duration) + + subscriber.increment(:cache_delete_duration, event.duration) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index f8c1d956ca..d6ae54e25e 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -26,4 +26,10 @@ describe Gitlab::Metrics::System do end end end + + describe '.cpu_time' do + it 'returns a Fixnum' do + expect(described_class.cpu_time).to be_an_instance_of(Fixnum) + end + end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 8f63a5f204..10177c0e8d 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -74,24 +74,21 @@ describe Gitlab::Metrics do let(:transaction) { Gitlab::Metrics::Transaction.new } before do - allow(Gitlab::Metrics::Transaction).to receive(:current). + allow(Gitlab::Metrics).to receive(:current_transaction). and_return(transaction) end it 'adds a metric to the current transaction' do - expect(transaction).to receive(:add_metric). - with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' }) + expect(transaction).to receive(:increment). + with('foo_real_time', a_kind_of(Numeric)) - Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 } - end + expect(transaction).to receive(:increment). + with('foo_cpu_time', a_kind_of(Numeric)) - it 'supports adding of custom values' do - values = { duration: a_kind_of(Numeric), number: 10 } + expect(transaction).to receive(:increment). + with('foo_call_count', 1) - expect(transaction).to receive(:add_metric). - with(:foo, values, { tag: 'value' }) - - Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 } + Gitlab::Metrics.measure(:foo) { 10 } end it 'returns the return value of the block' do @@ -101,4 +98,29 @@ describe Gitlab::Metrics do end end end + + describe '.tag_transaction' do + context 'without a transaction' do + it 'does nothing' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + not_to receive(:add_tag) + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::Metrics::Transaction.new } + + it 'adds the tag to the transaction' do + expect(Gitlab::Metrics).to receive(:current_transaction). + and_return(transaction) + + expect(transaction).to receive(:add_tag). + with(:foo, 'bar') + + Gitlab::Metrics.tag_transaction(:foo, 'bar') + end + end + end end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index da65267744..f093d0a0d8 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -4,13 +4,12 @@ describe 'Gitlab::NoteDataBuilder', lib: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::NoteDataBuilder.build(note, user) } - let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors before(:each) do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) - expect(data[:object_attributes][:url]).to eq(note_url) + expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note)) expect(data[:object_kind]).to eq('note') expect(data[:user]).to eq(user.hook_attrs) end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 3a769acfdc..6727a83e58 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -15,20 +15,20 @@ describe Gitlab::OAuth::User, lib: true do end let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } - describe :persisted? do + describe '#persisted?' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do expect( oauth_user.persisted? ).to be_truthy end - it "returns false if use is not found in database" do + it 'returns false if user is not found in database' do allow(auth_hash).to receive(:uid).and_return('non-existing') expect( oauth_user.persisted? ).to be_falsey end end - describe :save do + describe '#save' do def stub_omniauth_config(messages) allow(Gitlab.config.omniauth).to receive_messages(messages) end @@ -40,8 +40,27 @@ describe Gitlab::OAuth::User, lib: true do let(:provider) { 'twitter' } describe 'signup' do - shared_examples "to verify compliance with allow_single_sign_on" do - context "with new allow_single_sign_on enabled syntax" do + shared_examples 'to verify compliance with allow_single_sign_on' do + context 'provider is marked as external' do + it 'should mark user as external' do + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_truthy + end + end + + context 'provider was external, now has been removed' do + it 'should mark existing user internal' do + create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true) + stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook']) + oauth_user.save + expect(gl_user).to be_valid + expect(gl_user.external).to be_falsey + end + end + + context 'with new allow_single_sign_on enabled syntax' do before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } it "creates a user from Omniauth" do @@ -67,16 +86,16 @@ describe Gitlab::OAuth::User, lib: true do end end - context "with new allow_single_sign_on disabled syntax" do + context 'with new allow_single_sign_on disabled syntax' do before { stub_omniauth_config(allow_single_sign_on: []) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end - context "with old allow_single_sign_on disabled (Default)" do + context 'with old allow_single_sign_on disabled (Default)' do before { stub_omniauth_config(allow_single_sign_on: false) } - it "throws an error" do + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index f023be6ae4..6ffc0d6e65 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -1,77 +1,110 @@ require 'spec_helper' describe Gitlab::UrlBuilder, lib: true do - describe 'When asking for an issue' do - it 'returns the issue url' do - issue = create(:issue) - url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + describe '.build' do + context 'when passing a Commit' do + it 'returns a proper URL' do + commit = build_stubbed(:commit) + + url = described_class.build(commit) + + expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}" + end end - end - describe 'When asking for an merge request' do - it 'returns the merge request url' do - merge_request = create(:merge_request) - url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + context 'when passing an Issue' do + it 'returns a proper URL' do + issue = build_stubbed(:issue, iid: 42) + + url = described_class.build(issue) + + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" + end end - end - describe 'When asking for a note on commit' do - let(:note) { create(:note_on_commit) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a MergeRequest' do + it 'returns a proper URL' do + merge_request = build_stubbed(:merge_request, iid: 42) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + url = described_class.build(merge_request) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" + end end - end - describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'when passing a Note' do + context 'on a Commit' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" - end - end + url = described_class.build(note) - describe 'When asking for a note on issue' do - let(:issue) { create(:issue) } - let(:note) { create(:note_on_issue, noteable_id: issue.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" - end - end + context 'on a CommitDiff' do + it 'returns a proper URL' do + note = build_stubbed(:note_on_commit_diff) - describe 'When asking for a note on merge request' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + url = described_class.build(note) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end - describe 'When asking for a note on merge request diff' do - let(:merge_request) { create(:merge_request) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + context 'on an Issue' do + it 'returns a proper URL' do + issue = create(:issue, iid: 42) + note = build_stubbed(:note_on_issue, noteable: issue) - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" - end - end + url = described_class.build(note) - describe 'When asking for a note on project snippet' do - let(:snippet) { create(:project_snippet) } - let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } - let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end - it 'returns the note url' do - expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + context 'on a MergeRequest' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a MergeRequestDiff' do + it 'returns a proper URL' do + merge_request = create(:merge_request, iid: 42) + note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + context 'on a ProjectSnippet' do + it 'returns a proper URL' do + project_snippet = create(:project_snippet) + note = build_stubbed(:note_on_project_snippet, noteable: project_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + + context 'on another object' do + it 'returns a proper URL' do + project = build_stubbed(:project) + + expect { described_class.build(project) }. + to raise_error(NotImplementedError, 'No URL builder defined for Project') + end + end end end end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb new file mode 100644 index 0000000000..c59dfea5c5 --- /dev/null +++ b/spec/lib/gitlab_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Gitlab, lib: true do + describe '.com?' do + it 'is true when on GitLab.com' do + stub_config_setting(url: 'https://gitlab.com') + + expect(described_class.com?).to eq true + end + + it 'is false when not on GitLab.com' do + stub_config_setting(url: 'http://example.com') + + expect(described_class.com?).to eq false + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 15052aaca2..fac516f956 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -191,12 +191,19 @@ describe Issue, models: true do end describe '#related_branches' do - it "selects the right branches" do + it 'selects the right branches' do allow(subject.project.repository).to receive(:branch_names). - and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) + and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name]) expect(subject.related_branches).to eq([subject.to_branch_name]) end + + it 'excludes stable branches from the related branches' do + allow(subject.project.repository).to receive(:branch_names). + and_return(["#{subject.iid}-0-stable"]) + + expect(subject.related_branches).to eq [] + end end it_behaves_like 'an editable mentionable' do @@ -210,11 +217,11 @@ describe Issue, models: true do let(:subject) { create :issue } end - describe "#to_branch_name" do + describe '#to_branch_name' do let(:issue) { create(:issue, title: 'a' * 30) } - it "starts with the issue iid" do - expect(issue.to_branch_name).to match /-#{issue.iid}\z/ + it 'starts with the issue iid' do + expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/ end end end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb new file mode 100644 index 0000000000..295081e9da --- /dev/null +++ b/spec/models/notification_setting_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe NotificationSetting, type: :model do + describe "Associations" do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:source) } + end + + describe "Validation" do + subject { NotificationSetting.new(source_id: 1, source_type: 'Project') } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:level) } + it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) } + end +end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index c34b2487ec..31b2c90122 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -21,74 +21,232 @@ require 'spec_helper' describe BambooService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } + describe 'Validations' do + describe '#bamboo_url' do + it 'does not validate the presence of bamboo_url if service is not active' do + bamboo_service = service + bamboo_service.active = false - context "when a password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "reset password if url changed" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil - end - - it "does not reset password if username changed" do - @bamboo_service.username = "some_name" - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") + expect(bamboo_service).not_to validate_presence_of(:bamboo_url) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") - end + it 'validates the presence of bamboo_url if service is active' do + bamboo_service = service + bamboo_service.active = true - it "should reset password if url changed, even if setter called multiple times" do - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.bamboo_url = 'http://gitlab1.com' - @bamboo_service.save - expect(@bamboo_service.password).to be_nil + expect(bamboo_service).to validate_presence_of(:bamboo_url) end end - - context "when no password was previously set" do - before do - @bamboo_service = BambooService.create( - project: create(:project), - properties: { - bamboo_url: 'http://gitlab.com', - username: 'mic' - } - ) + + describe '#build_key' do + it 'does not validate the presence of build_key if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:build_key) end - it "saves password if new url is set together with password" do - @bamboo_service.bamboo_url = 'http://gitlab_edited.com' - @bamboo_service.password = 'password' - @bamboo_service.save - expect(@bamboo_service.password).to eq("password") - expect(@bamboo_service.bamboo_url).to eq("http://gitlab_edited.com") + it 'validates the presence of build_key if service is active' do + bamboo_service = service + bamboo_service.active = true + + expect(bamboo_service).to validate_presence_of(:build_key) + end + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:username) end + it 'does not validate the presence of username if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = nil + + expect(bamboo_service).not_to validate_presence_of(:username) + end + + it 'validates the presence of username if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.password = 'secret' + + expect(bamboo_service).to validate_presence_of(:username) + end + end + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + bamboo_service = service + bamboo_service.active = false + + expect(bamboo_service).not_to validate_presence_of(:password) + end + + it 'does not validate the presence of password if username is nil' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = nil + + expect(bamboo_service).not_to validate_presence_of(:password) + end + + it 'validates the presence of password if service is active and username is present' do + bamboo_service = service + bamboo_service.active = true + bamboo_service.username = 'john' + + expect(bamboo_service).to validate_presence_of(:password) + end end end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab1.com' + bamboo_service.save + + expect(bamboo_service.password).to be_nil + end + + it 'does not reset password if username changed' do + bamboo_service = service + + bamboo_service.username = 'some_name' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + bamboo_service = service + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') + end + end + + it 'saves password if new url is set together with password when no password was previously set' do + bamboo_service = service + bamboo_service.password = nil + + bamboo_service.bamboo_url = 'http://gitlab_edited.com' + bamboo_service.password = 'password' + bamboo_service.save + + expect(bamboo_service.password).to eq('password') + expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com') + end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a specific URL when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo') + end + + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + + it 'returns a build URL when bamboo_url has a trailing slash' do + stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + + expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build state contains Success' do + stub_request(build_state: 'YAY Success!') + + expect(service.commit_status('123', 'unused')).to eq('success') + end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(build_state: 'NO Failed!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(build_state: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(build_state: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(bamboo_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_state: 'success') + bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic' + body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) + + WebMock.stub_request(:get, bamboo_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb index 2ccbff553f..7c23c2efcc 100644 --- a/spec/models/project_services/builds_email_service_spec.rb +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe BuildsEmailService do let(:build) { create(:ci_build) } let(:data) { Gitlab::BuildDataBuilder.build(build) } - let(:service) { BuildsEmailService.new } + let!(:project) { create(:project, :public, ci_id: 1) } + let(:service) { described_class.new(project: project, active: true) } - describe :execute do + describe '#execute' do it 'sends email' do service.recipients = 'test@gitlab.com' data[:build_status] = 'failed' @@ -40,4 +41,36 @@ describe BuildsEmailService do service.execute(data) end end + + describe 'validations' do + + context 'when pusher is not added' do + before { service.add_pusher = false } + + it 'does not allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be false + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + + end + + context 'when pusher is added' do + before { service.add_pusher = true } + + it 'does allow empty recipient input' do + service.recipients = '' + expect(service.valid?).to be true + end + + it 'does allow non-empty recipient input' do + service.recipients = 'test@example.com' + expect(service.valid?).to be true + end + end + end end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f26b47a856..bc7423cee6 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -21,73 +21,220 @@ require 'spec_helper' describe TeamcityService, models: true do - describe "Associations" do + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project) } + describe 'Validations' do + describe '#teamcity_url' do + it 'does not validate the presence of teamcity_url if service is not active' do + teamcity_service = service + teamcity_service.active = false - context "when a password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic', - password: "password" - } - ) - end - - it "reset password if url changed" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil - end - - it "does not reset password if username changed" do - @teamcity_service.username = "some_name" - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") + expect(teamcity_service).not_to validate_presence_of(:teamcity_url) end - it "does not reset password if new url is set together with password, even if it's the same password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") - end + it 'validates the presence of teamcity_url if service is active' do + teamcity_service = service + teamcity_service.active = true - it "should reset password if url changed, even if setter called multiple times" do - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.teamcity_url = 'http://gitlab1.com' - @teamcity_service.save - expect(@teamcity_service.password).to be_nil + expect(teamcity_service).to validate_presence_of(:teamcity_url) end end - - context "when no password was previously set" do - before do - @teamcity_service = TeamcityService.create( - project: create(:project), - properties: { - teamcity_url: 'http://gitlab.com', - username: 'mic' - } - ) + + describe '#build_type' do + it 'does not validate the presence of build_type if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:build_type) end - it "saves password if new url is set together with password" do - @teamcity_service.teamcity_url = 'http://gitlab_edited.com' - @teamcity_service.password = 'password' - @teamcity_service.save - expect(@teamcity_service.password).to eq("password") - expect(@teamcity_service.teamcity_url).to eq("http://gitlab_edited.com") + it 'validates the presence of build_type if service is active' do + teamcity_service = service + teamcity_service.active = true + + expect(teamcity_service).to validate_presence_of(:build_type) + end + end + + describe '#username' do + it 'does not validate the presence of username if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:username) + end + + it 'does not validate the presence of username if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = nil + + expect(teamcity_service).not_to validate_presence_of(:username) + end + + it 'validates the presence of username if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.password = 'secret' + + expect(teamcity_service).to validate_presence_of(:username) + end + end + + describe '#password' do + it 'does not validate the presence of password if service is not active' do + teamcity_service = service + teamcity_service.active = false + + expect(teamcity_service).not_to validate_presence_of(:password) + end + + it 'does not validate the presence of password if username is nil' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = nil + + expect(teamcity_service).not_to validate_presence_of(:password) + end + + it 'validates the presence of password if service is active and username is present' do + teamcity_service = service + teamcity_service.active = true + teamcity_service.username = 'john' + + expect(teamcity_service).to validate_presence_of(:password) end end end + + describe 'Callbacks' do + describe 'before_update :reset_password' do + context 'when a password was previously set' do + it 'resets password if url changed' do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab1.com' + teamcity_service.save + + expect(teamcity_service.password).to be_nil + end + + it 'does not reset password if username changed' do + teamcity_service = service + + teamcity_service.username = 'some_name' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + end + + it "does not reset password if new url is set together with password, even if it's the same password" do + teamcity_service = service + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + + it 'saves password if new url is set together with password when no password was previously set' do + teamcity_service = service + teamcity_service.password = nil + + teamcity_service.teamcity_url = 'http://gitlab_edited.com' + teamcity_service.password = 'password' + teamcity_service.save + + expect(teamcity_service.password).to eq('password') + expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com') + end + end + end + + describe '#build_page' do + it 'returns a specific URL when status is 500' do + stub_request(status: 500) + + expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + + it 'returns a build URL when teamcity_url has a trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) + + expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo') + end + end + + describe '#commit_status' do + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') + + expect(service.commit_status('123', 'unused')).to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + expect(service.commit_status('123', 'unused')).to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + expect(service.commit_status('123', 'unused')).to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + expect(service.commit_status('123', 'unused')).to eq(:error) + end + end + + def service(teamcity_url: 'http://gitlab.com') + described_class.create( + project: build_stubbed(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + + def stub_request(status: 200, body: nil, build_status: 'success') + teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + body ||= %Q({"build":{"status":"#{build_status}","id":"666"}}) + + WebMock.stub_request(:get, teamcity_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4e49c413f2..c3a4016fa4 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -393,6 +393,8 @@ describe Repository, models: true do describe '#expire_cache' do it 'expires all caches' do expect(repository).to receive(:expire_branch_cache) + expect(repository).to receive(:expire_branch_count_cache) + expect(repository).to receive(:expire_tag_count_cache) repository.expire_cache end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 3e8b4aa1f8..96d89e6920 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -42,9 +42,10 @@ describe API::API, api: true do end end - it "users not part of the group should get access error" do + it 'users not part of the group should get access error' do get api("/groups/#{group_with_members.id}/members", stranger) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -165,12 +166,13 @@ describe API::API, api: true do end end - describe "DELETE /groups/:id/members/:user_id" do - context "when not a member of the group" do + describe 'DELETE /groups/:id/members/:user_id' do + context 'when not a member of the group' do it "should not delete guest's membership of group_with_members" do random_user = create(:user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 41c9cacd45..37ddab83c3 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -61,7 +61,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -92,9 +93,54 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}", user1) + + expect(response.status).to eq(404) + end + end + end + + describe 'PUT /groups/:id' do + let(:new_group_name) { 'New Group'} + + context 'when authenticated as the group owner' do + it 'updates the group' do + put api("/groups/#{group1.id}", user1), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + + it 'returns 404 for a non existing group' do + put api('/groups/1328', user1) + + expect(response.status).to eq(404) + end + end + + context 'when authenticated as the admin' do + it 'updates the group' do + put api("/groups/#{group1.id}", admin), name: new_group_name + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(new_group_name) + end + end + + context 'when authenticated as an user that can see the group' do + it 'does not updates the group' do + put api("/groups/#{group1.id}", user2), name: new_group_name + expect(response.status).to eq(403) end end + + context 'when authenticated as an user that cannot see the group' do + it 'returns 404 when trying to update the group' do + put api("/groups/#{group2.id}", user1), name: new_group_name + + expect(response.status).to eq(404) + end + end end describe "GET /groups/:id/projects" do @@ -113,7 +159,8 @@ describe API::API, api: true do it "should not return a group not attached to user1" do get api("/groups/#{group2.id}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end @@ -145,7 +192,8 @@ describe API::API, api: true do it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}/projects", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end end @@ -203,7 +251,8 @@ describe API::API, api: true do it "should not remove a group not attached to user1" do delete api("/groups/#{group2.id}", user1) - expect(response.status).to eq(403) + + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 822d3ad301..f88e39cad9 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } + let(:user2) { create(:user) } let(:non_member) { create(:user) } let(:author) { create(:author) } let(:assignee) { create(:assignee) } let(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, namespace: user.namespace ) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -320,13 +321,13 @@ describe API::API, api: true do end context 'when an admin or owner makes the request' do - it "accepts the creation date to be set" do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', created_at: 2.weeks.ago + title: 'new issue', labels: 'label, label2', created_at: creation_time 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 + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) end end end @@ -477,6 +478,18 @@ describe API::API, api: true do expect(json_response['labels']).to include 'label2' expect(json_response['state']).to eq "closed" end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label3', state_event: 'close', updated_at: update_time + expect(response.status).to eq(200) + + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_within(1.second).of(update_time) + end + end end describe "DELETE /projects/:id/issues/:issue_id" do @@ -501,4 +514,114 @@ describe API::API, api: true do end end end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id + + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + to_project_id: target_project2.id + + expect(response.status).to eq(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response.status).to eq(404) + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response.status).to eq(404) + end + end + end + + describe 'POST :id/issues/:issue_id/subscription' do + it 'subscribes to an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the issue is not found' do + post api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end + end + + describe 'DELETE :id/issues/:issue_id/subscription' do + it 'unsubscribes from an issue' do + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the issue is not found' do + delete api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response.status).to eq(404) + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 25fa30b2f2..1fa7e76894 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -516,6 +516,48 @@ describe API::API, api: true do end end + describe 'POST :id/merge_requests/:merge_request_id/subscription' do + it 'subscribes to a merge request' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response.status).to eq(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end + end + + describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do + it 'unsubscribes from a merge request' do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response.status).to eq(304) + end + + it 'returns 404 if the merge request is not found' do + post api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response.status).to eq(404) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index d97bf6d38f..344f0fe0b7 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -50,10 +50,12 @@ describe API::API, api: true do end it 'should return a project milestone by iid' do - get api("/projects/#{project.id}/milestones?iid=#{milestone.iid}", user) + get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) + expect(response.status).to eq 200 - expect(json_response.first['title']).to eq milestone.title - expect(json_response.first['id']).to eq milestone.id + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq closed_milestone.title + expect(json_response.first['id']).to eq closed_milestone.id end it 'should return 401 error if user not authenticated' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 39f9a06fe1..ec9eda0a2e 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -158,6 +158,19 @@ describe API::API, api: true do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' expect(response.status).to eq(401) end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) + expect(Time.parse(json_response['created_at'])).to be_within(1.second).of(creation_time) + end + end + end context "when noteable is a Snippet" do @@ -241,4 +254,65 @@ describe API::API, api: true do end end + describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do + context 'when noteable is an Issue' do + it 'deletes a note' do + delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + + expect(response.status).to eq(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + "notes/#{issue_note.id}", user) + expect(response.status).to eq(404) + end + + it 'returns a 404 error when note id not found' do + delete api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) + + expect(response.status).to eq(404) + end + end + + context 'when noteable is a Snippet' do + it 'deletes a note' do + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + + expect(response.status).to eq(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/#{snippet_note.id}", user) + expect(response.status).to eq(404) + end + + it 'returns a 404 error when note id not found' do + delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ + "notes/123", user) + + expect(response.status).to eq(404) + end + end + + context 'when noteable is a Merge Request' do + it 'deletes a note' do + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + + expect(response.status).to eq(200) + # Check if note is really deleted + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/#{merge_request_note.id}", user) + expect(response.status).to eq(404) + end + + it 'returns a 404 error when note id not found' do + delete api("/projects/#{project.id}/merge_requests/"\ + "#{merge_request.id}/notes/123", user) + + expect(response.status).to eq(404) + end + end + end + end diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 4301588b16..c112ca5e3c 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -118,8 +118,10 @@ describe API::API, api: true do end describe "DELETE /projects/:id/members/:user_id" do - before { project_member } - before { project_member2 } + before do + project_member + project_member2 + end it "should remove user from project team" do expect do @@ -132,6 +134,7 @@ describe API::API, api: true do expect do delete api("/projects/#{project.id}/members/#{user3.id}", user) end.to_not change { ProjectMember.count } + expect(response.status).to eq(200) end it "should return 200 if team member already removed" do @@ -145,8 +148,19 @@ describe API::API, api: true do delete api("/projects/#{project.id}/members/1000000", user) end.to change { ProjectMember.count }.by(0) expect(response.status).to eq(200) - expect(json_response['message']).to eq("Access revoked") expect(json_response['id']).to eq(1000000) + expect(json_response['message']).to eq('Access revoked') + end + + context 'when the user is not an admin or owner' do + it 'can leave the project' do + expect do + delete api("/projects/#{project.id}/members/#{user3.id}", user3) + end.to change { ProjectMember.count }.by(-1) + + expect(response.status).to eq(200) + expect(json_response['id']).to eq(project_member2.id) + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index be2034e0f3..fccd08bd6d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1020,6 +1020,54 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/star' do + context 'on an unstarred project' do + it 'stars the project' do + expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + + expect(response.status).to eq(201) + expect(json_response['star_count']).to eq(1) + end + end + + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'does not modify the star count' do + expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response.status).to eq(304) + end + end + end + + describe 'DELETE /projects/:id/star' do + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'unstars the project' do + expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + + expect(response.status).to eq(200) + expect(json_response['star_count']).to eq(0) + end + end + + context 'on an unstarred project' do + it 'does not modify the star count' do + expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response.status).to eq(304) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index a15be07ed5..9f9c3b1cf4 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -40,6 +40,23 @@ describe API::API, api: true do end end + describe 'GET /projects/:id/repository/tags/:tag_name' do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + + it 'returns a specific tag' do + get api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(tag_name) + end + + it 'returns 404 for an invalid tag name' do + get api("/projects/#{project.id}/repository/tags/foobar", user) + + expect(response.status).to eq(404) + end + end + describe 'POST /projects/:id/repository/tags' do context 'lightweight tags' do it 'should create a new tag' do diff --git a/spec/services/notes/delete_service_spec.rb b/spec/services/notes/delete_service_spec.rb new file mode 100644 index 0000000000..1d0a747a48 --- /dev/null +++ b/spec/services/notes/delete_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Notes::DeleteService, services: true do + describe '#execute' do + it 'deletes a note' do + project = create(:empty_project) + issue = create(:issue, project: project) + note = create(:note, project: project, noteable: issue) + + described_class.new(project, note.author).execute(note) + + expect(project.issues.find(issue.id).notes).not_to include(note) + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 0f2aa3ae73..d7c72dc081 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -88,12 +88,9 @@ describe NotificationService, services: true do note.project.namespace_id = group.id note.project.group.add_user(@u_watcher, GroupMember::MASTER) note.project.save - user_project = note.project.project_members.find_by_user_id(@u_watcher.id) - user_project.notification_level = Notification::N_PARTICIPATING - user_project.save - group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) - group_member.notification_level = Notification::N_GLOBAL - group_member.save + + @u_watcher.notification_settings_for(note.project).participating! + @u_watcher.notification_settings_for(note.project.group).global! ActionMailer::Base.deliveries.clear end @@ -215,7 +212,7 @@ describe NotificationService, services: true do end it do - @u_committer.update_attributes(notification_level: Notification::N_MENTION) + @u_committer.update_attributes(notification_level: :mention) notification.new_note(note) should_not_email(@u_committer) end @@ -246,7 +243,7 @@ describe NotificationService, services: true do end it do - issue.assignee.update_attributes(notification_level: Notification::N_MENTION) + issue.assignee.update_attributes(notification_level: :mention) notification.new_issue(issue, @u_disabled) should_not_email(issue.assignee) @@ -596,13 +593,13 @@ describe NotificationService, services: true do end def build_team(project) - @u_watcher = create(:user, notification_level: Notification::N_WATCH) - @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) - @u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING) - @u_disabled = create(:user, notification_level: Notification::N_DISABLED) - @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION) + @u_watcher = create(:user, notification_level: :watch) + @u_participating = create(:user, notification_level: :participating) + @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating) + @u_disabled = create(:user, notification_level: :disabled) + @u_mentioned = create(:user, username: 'mention', notification_level: :mention) @u_committer = create(:user, username: 'committer') - @u_not_mentioned = create(:user, username: 'regular', notification_level: Notification::N_PARTICIPATING) + @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating) @u_outsider_mentioned = create(:user, username: 'outsider') project.team << [@u_watcher, :master] @@ -617,8 +614,8 @@ describe NotificationService, services: true do def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user - @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) - @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH) + @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating) + @watcher_and_subscriber = create(:user, notification_level: :watch) project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index c46259431a..0601731733 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -38,4 +38,27 @@ describe Projects::TransferService, services: true do def transfer_project(project, user, new_namespace) Projects::TransferService.new(project, user).execute(new_namespace) end + + context 'visibility level' do + let(:internal_group) { create(:group, :internal) } + + before { internal_group.add_owner(user) } + + context 'when namespace visibility level < project visibility level' do + let(:public_project) { create(:project, :public, namespace: user.namespace) } + + before { transfer_project(public_project, user, internal_group) } + + it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } + end + + context 'when namespace visibility level > project visibility level' do + let(:private_project) { create(:project, :private, namespace: user.namespace) } + + before { transfer_project(private_project, user, internal_group) } + + it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } + end + end + end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 0265dbe9c6..94ff345790 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -4,6 +4,9 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } + let(:project) { create(:project) } + let(:key) { create(:key, user: project.owner) } + let(:key_id) { key.shell_id } context "as a resque worker" do it "reponds to #perform" do @@ -11,11 +14,43 @@ describe PostReceive do end end - context "webhook" do - let(:project) { create(:project) } - let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + describe "#process_project_changes" do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } + + it "should call GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } + + it "should not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + PostReceive.new.perform(pwd(project), key_id, base64_changes) + end + end + end + + context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js new file mode 100644 index 0000000000..f5dc4abcd8 --- /dev/null +++ b/vendor/assets/javascripts/date.format.js @@ -0,0 +1,125 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + +var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; +}(); + +// Some common format strings +dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" +}; + +// Internationalization strings +dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] +}; + +// For convenience... +Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); +};