diff options
author | Fatih Acet <acetfatih@gmail.com> | 2016-06-06 18:58:24 +0300 |
---|---|---|
committer | Fatih Acet <acetfatih@gmail.com> | 2016-06-06 18:58:24 +0300 |
commit | 3f2db9d971dd92b446e2a615ed758090079a8c64 (patch) | |
tree | c3034fc5bde8c3d0d2516de01e67d44c0c796fa1 | |
parent | 5b43eeee97f8e06a4ada4b173cb972c20d58d8ed (diff) | |
parent | ac4e3e8cf016f2c2b3dcf3997673310a5df658a9 (diff) | |
download | gitlab-ce-button-focus-state.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into button-focus-statebutton-focus-state
245 files changed, 4955 insertions, 1404 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 9275f9537d5..eb51a04c0ec 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -194,7 +194,7 @@ Style/EmptyLines: # Keep blank lines around access modifiers. Style/EmptyLinesAroundAccessModifier: - Enabled: false + Enabled: true # Keeps track of empty lines around block bodies. Style/EmptyLinesAroundBlockBody: @@ -247,7 +247,7 @@ Style/FlipFlop: # Checks use of for or each in multiline loops. Style/For: - Enabled: false + Enabled: true # Enforce the use of Kernel#sprintf, Kernel#format or String#%. Style/FormatString: @@ -514,7 +514,7 @@ Style/SelfAssignment: # Don't use semicolons to terminate expressions. Style/Semicolon: - Enabled: false + Enabled: true # Checks for proper usage of fail and raise. Style/SignalException: @@ -771,7 +771,7 @@ Metrics/PerceivedComplexity: # Checks for ambiguous operators in the first argument of a method invocation # without parentheses. Lint/AmbiguousOperator: - Enabled: false + Enabled: true # Checks for ambiguous regexp literals in the first argument of a method # invocation without parentheses. diff --git a/.vagrant_enabled b/.vagrant_enabled new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/.vagrant_enabled diff --git a/CHANGELOG b/CHANGELOG index 848aaa8506e..fe9b9bec868 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,37 +1,69 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.9.0 (unreleased) + - Bulk assign/unassign labels to issues. - Allow enabling wiki page events from Webhook management UI - Make EmailsOnPushWorker use Sidekiq mailers queue - Fix wiki page events' webhook to point to the wiki repository - Fix issue todo not remove when leave project !4150 (Long Nguyen) - Allow forking projects with restricted visibility level - Improve note validation to prevent errors when creating invalid note via API + - Reduce number of fog gem dependencies - Remove project notification settings associated with deleted projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Redesign navigation for project pages - Fix groups API to list only user's accessible projects - Redesign account and email confirmation emails - Use gitlab-shell v3.0.0 + - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged + - Don't allow MRs to be merged when commits were added since the last review / page load - Add DB index on users.state - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Fix issues filter when ordering by milestone - Todos will display target state if issuable target is 'Closed' or 'Merged' - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - Add support for using Yubikeys (U2F) for two-factor authentication + - Link to blank group icon doesn't throw a 404 anymore - Remove 'main language' feature - Pipelines can be canceled only when there are running builds + - Use downcased path to container repository as this is expected path by Docker - Projects pending deletion will render a 404 page - Measure queue duration between gitlab-workhorse and Rails - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Cache assigned issue and merge request counts in sidebar nav + - Cache project build count in sidebar nav + - Reduce number of queries needed to render issue labels in the sidebar + - Improve error handling importing projects + - Put project Files and Commits tabs under Code tab + - Replace Colorize with Rainbow for coloring console output in Rake tasks. + +v 8.8.4 (unreleased) + - Ensure branch cleanup regardless of whether the GitHub import process succeeds + - Fix issue with arrow keys not working in search autocomplete dropdown + - Fix todos page throwing errors when you have a project pending deletion + - Reduce number of SQL queries when rendering user references + - Upgrade to jQuery 2 v 8.8.3 - - Fix incorrect links on pipeline page when merge request created from fork - - Fix gitlab importer failing to import new projects due to missing credentials - - Fix import URL migration not rescuing with the correct Error - - In search results, only show notes on confidential issues that the user has access to - - Fix health check access token changing due to old application settings being used + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 + - Fixed JS error when trying to remove discussion form. !4303 + - Fixed issue with button color when no CI enabled. !4287 + - Fixed potential issue with 2 CI status polling events happening. !3869 + - Improve design of Pipeline view. !4230 + - Fix gitlab importer failing to import new projects due to missing credentials. !4301 + - Fix import URL migration not rescuing with the correct Error. !4321 + - Fix health check access token changing due to old application settings being used. !4332 + - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363 + - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364 + - Pass the "Remember me" value to the 2FA token form. !4369 + - Fix incorrect links on pipeline page when merge request created from fork. !4376 + - Use downcased path to container repository as this is expected path by Docker. !4420 + - Fix wiki project clone address error (chujinjin). !4429 + - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 + - Fix missing number on generated ordered list element. !4437 + - Prevent disclosure of notes on confidential issues in search results. v 8.8.2 - Added remove due date button. !4209 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fe4cf7b0f6..18270d9598f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design (the PNG is 1:1). -The current designs can be found in the [`gitlab1.atype` file]. +The current designs can be found in the [`gitlab8.atype` file]. ### UI development kit @@ -308,16 +308,14 @@ tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: 1. Fork the project into your personal space on GitLab.com -1. Create a feature branch +1. Create a feature branch, branch away from `master`. 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) -1. If you are changing the README, some documentation or other things which - have no effect on the tests, add `[ci skip]` somewhere in the commit message - and make sure to read the [documentation styleguide][doc-styleguide] +1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by [squashing them][git-squash] 1. Push the commit(s) to your fork -1. Submit a merge request (MR) to the master branch +1. Submit a merge request (MR) to the `master` branch 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you used to achieve it, see the [merge request description format] @@ -532,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 -[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ +[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ @@ -18,9 +18,8 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem 'devise', '~> 3.5.4' +gem 'devise', '~> 4.0' gem 'doorkeeper', '~> 3.1' -gem 'devise-async', '~> 0.9.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' @@ -43,12 +42,13 @@ gem 'recaptcha', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' # Two-factor authentication -gem 'devise-two-factor', '~> 2.0.0' +gem 'devise-two-factor', '~> 3.0.0' gem 'rqrcode-rails3', '~> 0.1.7' -gem 'attr_encrypted', '~> 1.3.4' +gem 'attr_encrypted', '~> 3.0.0' +gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 1.0.0' +gem "browser", '~> 2.0.3' # Extracting information from a git repository # Provide access to Gitlab::Git library @@ -84,8 +84,14 @@ gem "carrierwave", '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' +# for backups +gem 'fog-aws', '~> 0.9' +gem 'fog-core', '~> 1.40' +gem 'fog-local', '~> 0.3' +gem 'fog-google', '~> 0.3' +gem 'fog-openstack', '~> 0.1' + # for aws storage -gem "fog", "~> 1.36.0" gem "unf", '~> 0.1.4' # Authorization @@ -138,7 +144,7 @@ gem 'redis-namespace' gem "httparty", '~> 0.13.3' # Colored output to console -gem "colorize", '~> 0.7.0' +gem "rainbow", '~> 2.1.0' # GitLab settings gem 'settingslogic', '~> 2.0.9' diff --git a/Gemfile.lock b/Gemfile.lock index 146e95167b2..5f1dbd431e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,6 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (4.0.2) actionmailer (4.2.6) @@ -60,8 +59,8 @@ GEM oauth2 (~> 1.0) asciidoctor (1.5.3) ast (2.2.0) - attr_encrypted (1.3.4) - encryptor (>= 1.3.0) + attr_encrypted (3.0.1) + encryptor (~> 3.0.0) attr_required (1.0.0) autoprefixer-rails (6.2.3) execjs @@ -73,7 +72,7 @@ GEM thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) - bcrypt (3.1.10) + bcrypt (3.1.11) benchmark-ips (2.3.0) better_errors (1.0.1) coderay (>= 1.0.0) @@ -93,7 +92,7 @@ GEM sass (~> 3.0) slim (>= 1.3.6, < 4.0) terminal-table (~> 1.4) - browser (1.0.1) + browser (2.0.3) builder (3.2.2) bullet (5.0.0) activesupport (>= 3.0.0) @@ -155,21 +154,18 @@ GEM activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.4) + devise (4.1.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 5.1) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) - devise-two-factor (2.0.1) + devise-two-factor (3.0.0) activesupport - attr_encrypted (~> 1.3.2) - devise (~> 3.5.0) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) railties - rotp (~> 2) + rotp (~> 2.0) diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) @@ -181,12 +177,12 @@ GEM email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) - encryptor (1.3.0) + encryptor (3.0.0) equalizer (0.0.11) erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.45.4) + excon (0.49.0) execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.5.0) @@ -203,8 +199,6 @@ GEM multi_json ffaker (2.0.0) ffi (1.9.10) - fission (0.5.0) - CFPropertyList (~> 2.2) flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) @@ -214,109 +208,28 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog (1.36.0) - fog-aliyun (>= 0.1.0) - fog-atmos - fog-aws (>= 0.6.0) - fog-brightbox (~> 0.4) - fog-core (~> 1.32) - fog-dynect (~> 0.0.2) - fog-ecloud (~> 0.1) - fog-google (<= 0.1.0) - fog-json - fog-local - fog-powerdns (>= 0.1.1) - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-riakcs - fog-sakuracloud (>= 0.0.4) - fog-serverlove - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xenserver - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) - fog-core (~> 1.27) - fog-json (~> 1.0) - ipaddress (~> 0.8) - xml-simple (~> 1.1) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-aws (0.8.1) + fog-aws (0.9.2) fog-core (~> 1.27) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-brightbox (0.10.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + fog-core (1.40.0) builder - excon (~> 0.45) + excon (~> 0.49) formatador (~> 0.2) - fog-dynect (0.0.2) - fog-core - fog-json - fog-xml - fog-ecloud (0.3.0) - fog-core - fog-xml - fog-google (0.1.0) + fog-google (0.3.2) fog-core fog-json fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.2.1) - fog-core (~> 1.27) - fog-powerdns (0.1.1) + fog-local (0.3.0) fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.5) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-riakcs (0.1.0) - fog-core - fog-json - fog-xml - fog-sakuracloud (1.7.5) - fog-core - fog-json - fog-serverlove (0.1.2) - fog-core - fog-json - fog-softlayer (1.0.3) - fog-core - fog-json - fog-storm_on_demand (0.1.1) - fog-core - fog-json - fog-terremark (0.1.0) - fog-core - fog-xml - fog-vmfusion (0.1.0) - fission - fog-core - fog-voxel (0.1.0) - fog-core - fog-xml - fog-xenserver (0.2.2) - fog-core - fog-xml + fog-openstack (0.1.6) + fog-core (>= 1.39) + fog-json (>= 1.0) + ipaddress (>= 0.8) fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) @@ -425,11 +338,10 @@ GEM httpclient (2.7.0.1) i18n (0.7.0) ice_nine (0.11.1) - inflecto (0.0.2) influxdb (0.2.3) cause json - ipaddress (0.8.2) + ipaddress (0.8.3) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) @@ -656,7 +568,7 @@ GEM responders (2.1.1) railties (>= 4.2.0, < 5.1) rinku (1.7.3) - rotp (2.1.1) + rotp (2.1.2) rouge (1.10.1) rqrcode (0.7.0) chunky_png @@ -835,6 +747,7 @@ GEM simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) + u2f (0.2.1) uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) @@ -859,7 +772,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.4) + warden (1.2.6) rack (>= 1.0) web-console (2.3.0) activemodel (>= 4.0) @@ -876,7 +789,6 @@ GEM builder expression_parser rinku - xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -894,7 +806,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - attr_encrypted (~> 1.3.4) + attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) base32 (~> 0.3.0) @@ -903,7 +815,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.2.0) - browser (~> 1.0.0) + browser (~> 2.0.3) bullet bundler-audit byebug @@ -912,16 +824,14 @@ DEPENDENCIES carrierwave (~> 0.10.0) charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) - colorize (~> 0.7.0) connection_pool (~> 2.0) coveralls (~> 0.8.2) creole (~> 0.5.0) d3_rails (~> 3.5.0) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (~> 3.5.4) - devise-async (~> 0.9.0) - devise-two-factor (~> 2.0.0) + devise (~> 4.0) + devise-two-factor (~> 3.0.0) diffy (~> 3.0.3) doorkeeper (~> 3.1) dropzonejs-rails (~> 0.7.1) @@ -931,7 +841,11 @@ DEPENDENCIES ffaker (~> 2.0.0) flay flog - fog (~> 1.36.0) + fog-aws (~> 0.9) + fog-core (~> 1.40) + fog-google (~> 0.3) + fog-local (~> 0.3) + fog-openstack (~> 0.1) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) @@ -1000,6 +914,7 @@ DEPENDENCIES rack-oauth2 (~> 1.2.1) rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) + rainbow (~> 2.1.0) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) @@ -1049,6 +964,7 @@ DEPENDENCIES thin (~> 1.6.1) tinder (~> 1.10.0) turbolinks (~> 2.5.0) + u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) unf (~> 0.1.4) @@ -1061,4 +977,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.12.4 + 1.12.5 diff --git a/README.md b/README.md index 418d06a45a5..fee93d5f9c3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # GitLab [![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) -[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) -[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) ## Canonical source diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 8f275510bad..7526398dadc 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -4,7 +4,7 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # -#= require jquery +#= require jquery2 #= require jquery-ui/autocomplete #= require jquery-ui/datepicker #= require jquery-ui/draggable @@ -18,7 +18,6 @@ #= require jquery.atwho #= require jquery.scrollTo #= require jquery.turbolinks -#= require d3 #= require turbolinks #= require autosave #= require bootstrap/affix @@ -51,9 +50,17 @@ #= require shortcuts_network #= require jquery.nicescroll #= require date.format -#= require_tree . +#= require_directory ./behaviors +#= require_directory ./blob +#= require_directory ./ci +#= require_directory ./commit +#= require_directory ./extensions +#= require_directory ./lib +#= require_directory ./u2f +#= require_directory . #= require fuzzaldrin-plus #= require cropper +#= require u2f window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index bf95e06b4e5..766c653111a 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,201 +1,300 @@ class @AwardsHandler - constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) -> - $('.js-add-award').on 'click', (event) => - event.stopPropagation() - event.preventDefault() - @showEmojiMenu() + constructor: -> + + @aliases = emojiAliases() + + $(document) + .off 'click', '.js-add-award' + .on 'click', '.js-add-award', (event) => + event.stopPropagation() + event.preventDefault() + + @showEmojiMenu $(event.currentTarget) $('html').on 'click', (event) -> - if !$(event.target).closest('.emoji-menu').length + unless $(event.target).closest('.emoji-menu').length if $('.emoji-menu').is(':visible') + $('.js-add-award.is-active').removeClass 'is-active' $('.emoji-menu').removeClass 'is-visible' - $('.awards') - .off 'click' - .on 'click', '.js-emoji-btn', @handleClick + $(document) + .off 'click', '.js-emoji-btn' + .on 'click', '.js-emoji-btn', @handleClick - @renderFrequentlyUsedBlock() - handleClick: (e) -> + handleClick: (e) => + e.preventDefault() - emoji = $(this) - .find('.icon') - .data 'emoji' - if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' - awardsHandler.addAward 'thumbsdown' + emoji = $(e.currentTarget).find('.icon').data 'emoji' + @getVotesBlock().addClass 'js-awards-block' + @addAward @getAwardUrl(), emoji - else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup' - awardsHandler.addAward 'thumbsup' - awardsHandler.addAward emoji + showEmojiMenu: ($addBtn) -> - $(this).trigger 'blur' + $menu = $('.emoji-menu') - 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 + if $menu.length + $holder = $addBtn.closest('.js-award-holder') - showEmojiMenu: -> - if $('.emoji-menu').length - if $('.emoji-menu').is '.is-visible' - $('.emoji-menu').removeClass 'is-visible' + if $menu.is '.is-visible' + $addBtn.removeClass 'is-active' + $menu.removeClass 'is-visible' $('#emoji_search').blur() else - $('.emoji-menu').addClass 'is-visible' + $addBtn.addClass 'is-active' + @positionMenu($menu, $addBtn) + + $menu.addClass 'is-visible' $('#emoji_search').focus() else - $('.js-add-award').addClass 'is-loading' - $.get @getEmojisUrl, (response) => - $('.js-add-award').removeClass 'is-loading' - $('.js-award-holder').append response + $addBtn.addClass 'is-loading is-active' + url = $addBtn.data 'award-menu-url' + + @createEmojiMenu url, => + $addBtn.removeClass 'is-loading' + $menu = $('.emoji-menu') + @positionMenu($menu, $addBtn) + @renderFrequentlyUsedBlock() + setTimeout => - $('.emoji-menu').addClass 'is-visible' + $menu.addClass 'is-visible' $('#emoji_search').focus() @setupSearch() , 200 - addAward: (emoji) -> - @postEmoji emoji, => - @addAwardToEmojiBar(emoji) + + createEmojiMenu: (awardMenuUrl, callback) -> + + $.get awardMenuUrl, (response) => + $('body').append response + callback() + + + positionMenu: ($menu, $addBtn) -> + position = $addBtn.data('position') + + # The menu could potentially be off-screen or in a hidden overflow element + # So we position the element absolute in the body + css = + top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" + + if position? and position is 'right' + css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" + $menu.addClass 'is-aligned-right' + else + css.left = "#{$addBtn.offset().left}px" + $menu.removeClass 'is-aligned-right' + + $menu.css(css) + + + addAward: (awardUrl, emoji, checkMutuality = yes) -> + + emoji = @normilizeEmojiName(emoji) + @postEmoji awardUrl, emoji, => + @addAwardToEmojiBar(emoji, checkMutuality) + + $('.js-awards-block-current').removeClass 'js-awards-block-current' $('.emoji-menu').removeClass 'is-visible' - addAwardToEmojiBar: (emoji) -> + + addAwardToEmojiBar: (emoji, checkForMutuality = yes) -> + + @checkMutuality emoji if checkForMutuality @addEmojiToFrequentlyUsedList(emoji) - if @exist(emoji) - if @isActive(emoji) - @decrementCounter(emoji) + emoji = @normilizeEmojiName(emoji) + $emojiBtn = @findEmojiIcon(emoji).parent() + + if $emojiBtn.length > 0 + if @isActive($emojiBtn) + @decrementCounter($emojiBtn, emoji) else - counter = @findEmojiIcon(emoji).siblings('.js-counter') + counter = $emojiBtn.find('.js-counter') counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass('active') - @addMeToAuthorList(emoji) + $emojiBtn.addClass('active') + @addMeToUserList(emoji) else @createEmoji(emoji) - exist: (emoji) -> - @findEmojiIcon(emoji).length > 0 - - isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass('active') - - decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings('.js-counter') - emojiIcon = counter.parent() - if parseInt(counter.text()) > 1 - counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) - else if emoji == 'thumbsup' || emoji == 'thumbsdown' - emojiIcon.tooltip('destroy') - counter.text(0) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) + + getVotesBlock: -> return $ '.awards.js-awards-block' + + + getAwardUrl: -> @getVotesBlock().data 'award-url' + + + checkMutuality: (emoji) -> + + awardUrl = @getAwardUrl() + + if emoji in [ 'thumbsup', 'thumbsdown' ] + mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + + isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active' + @addAward awardUrl, mutualVote, no if isAlreadyVoted + + + isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active' + + + decrementCounter: ($emojiBtn, emoji) -> + isntNoteBody = $emojiBtn.closest('.note-body').length is 0 + counter = $('.js-counter', $emojiBtn) + counterNumber = parseInt(counter.text()) + + if !isntNoteBody + # If this is a note body, we just hide the award emoji row like the initial state + $emojiBtn.closest('.js-awards-block').addClass 'hidden' + + if counterNumber > 1 + counter.text(counterNumber - 1) + @removeMeFromUserList($emojiBtn, emoji) + else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody + $emojiBtn.tooltip('destroy') + counter.text('0') + @removeMeFromUserList($emojiBtn, emoji) else - emojiIcon.tooltip('destroy') - emojiIcon.remove() + $emojiBtn.tooltip('destroy') + $emojiBtn.remove() + + $emojiBtn.removeClass('active') + + + getAwardTooltip: ($awardBlock) -> + + return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') + + + removeMeFromUserList: ($emojiBtn, emoji) -> + + awardBlock = $emojiBtn + originalTitle = @getAwardTooltip awardBlock + + authors = originalTitle.split ', ' + authors.splice authors.indexOf('me'), 1 + + newAuthors = authors.join ', ' - removeMeFromAuthorList: (emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() - authors = awardBlock - .attr('data-original-title') - .split(', ') - authors.splice(authors.indexOf('me'),1) awardBlock - .closest('.js-emoji-btn') - .attr('data-original-title', authors.join(', ')) + .closest '.js-emoji-btn' + .removeData 'original-title' + .removeData 'title' + .attr 'data-original-title', newAuthors + .attr 'data-title', newAuthors + @resetTooltip(awardBlock) - addMeToAuthorList: (emoji) -> + + addMeToUserList: (emoji) -> + awardBlock = @findEmojiIcon(emoji).parent() - origTitle = awardBlock.attr('data-original-title').trim() - authors = [] + origTitle = @getAwardTooltip awardBlock + users = [] + if origTitle - authors = origTitle.split(', ') - authors.push('me') - awardBlock.attr('data-original-title', authors.join(', ')) + users = origTitle.trim().split(', ') + + users.push('me') + awardBlock.attr('title', users.join(', ')) + @resetTooltip(awardBlock) + resetTooltip: (award) -> award.tooltip('destroy') - # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. + # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> award.tooltip() ), 200 - createEmoji: (emoji) -> - emojiCssClass = @resolveNameToCssClass(emoji) - - nodes = [] - nodes.push( - "<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>", - "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", - "<span class='award-control-text js-counter'>1</span>", - "</button>" - ) - - $(nodes.join("\n")) - .insertBefore('.js-award-holder') - .find('.emoji-icon') - .data('emoji', emoji) + createEmoji_: (emoji) -> + + emojiCssClass = @resolveNameToCssClass emoji + + buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> + <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div> + <span class='award-control-text js-counter'>1</span> + </button>" + + emoji_node = $(buttonHtml) + .insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)' + .find '.emoji-icon' + .data 'emoji', emoji + $('.award-control').tooltip() + $currentBlock = $ '.js-awards-block' + + if $currentBlock.is '.hidden' + $currentBlock.removeClass 'hidden' + + + createEmoji: (emoji) -> + + return @createEmoji_ emoji if $('.emoji-menu').length + + awardMenuUrl = gl.awardMenuUrl or '/emojis' + @createEmojiMenu awardMenuUrl, => @createEmoji emoji + + resolveNameToCssClass: (emoji) -> - emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") - if emojiIcon.length > 0 - unicodeName = emojiIcon.data('unicode-name') + emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") + + if emoji_icon.length > 0 + unicodeName = emoji_icon.data('unicode-name') else # Find by alias unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') - "emoji-#{unicodeName}" + return "emoji-#{unicodeName}" - postEmoji: (emoji, callback) -> - $.post @postEmojiUrl, { note: { - note: ":#{emoji}:" - noteable_type: @noteableType - noteable_id: @noteableId - }},(data) -> + + postEmoji: (awardUrl, emoji, callback) -> + $.post awardUrl, { name: emoji }, (data) -> if data.ok callback.call() findEmojiIcon: (emoji) -> - $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") + $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ scrollTop: $('.awards').offset().top - 80 }, 200) + normilizeEmojiName: (emoji) -> + @aliases[emoji] || emoji + addEmojiToFrequentlyUsedList: (emoji) -> - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push(emoji) - $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) + frequently_used_emojis = @getFrequentlyUsedEmojis() + frequently_used_emojis.push(emoji) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') - _.compact(_.uniq(frequentlyUsedEmojis)) + frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',') + _.compact(_.uniq(frequently_used_emojis)) renderFrequentlyUsedBlock: -> if $.cookie('frequently_used_emojis') - frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequently_used_emojis = @getFrequentlyUsedEmojis() - ul = $('<ul>') + ul = $("<ul class='clearfix emoji-menu-list'>") - for emoji in frequentlyUsedEmojis - do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) + for emoji in frequently_used_emojis + $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) setupSearch: -> - $('input.emoji-search').keyup (ev) => + $('input.emoji-search').on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results @@ -204,12 +303,12 @@ class @AwardsHandler if term # Generate a search result block h5 = $('<h5>').text('Search results').addClass('emoji-search') - foundEmojis = @searchEmojis(term).show() - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) + found_emojis = @searchEmojis(term).show() + ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis) $('.emoji-menu-content ul, .emoji-menu-content h5').hide() $('.emoji-menu-content').append(h5).append(ul) else $('.emoji-menu-content').children().show() searchEmojis: (term)-> - $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() + $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index a3185f87640..bae67a2ebaf 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,11 +17,13 @@ class Dispatcher switch page when 'projects:issues:index' Issuable.init() + new IssuableBulkActions() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() + window.awardsHandler = new AwardsHandler() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'dashboard:todos:index' @@ -52,6 +54,7 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() + window.awardsHandler = new AwardsHandler() when "projects:merge_requests:diffs" new Diff() new ZenMode() diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index 3cc70185178..3d009a96d05 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -21,7 +21,7 @@ class @DueDateSelect $dropdown.glDropdown( hidden: -> $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') ) addDueDate = (isDropdown) -> @@ -42,12 +42,13 @@ class @DueDateSelect type: 'PUT' url: issueUpdateURL data: data + dataType: 'json' beforeSend: -> $loading.fadeIn() if isDropdown $dropdown.trigger('loading.gl.dropdown') $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') $valueContent.html(mediumDate) $sidebarValue.html(mediumDate) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index 5de012e409f..4f73d215b85 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,5 +1,5 @@ class @Flash - constructor: (message, type)-> + constructor: (message, type = 'alert')-> @flash = $(".flash-container") @flash.html("") diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index b3f1dc969b8..7c7334e9e40 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -11,6 +11,8 @@ class GitLabDropdownFilter $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') + @indeterminateIds = [] + # Clear click $clearButton.on 'click', (e) => e.preventDefault() @@ -35,20 +37,20 @@ class GitLabDropdownFilter if keyCode is 13 return false - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - search_text = @input.val() + # Only filter asynchronously only if option remote is set + if @options.remote + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur keyCode - if blur_field and @filterInputBlur - @input.blur() + if blur_field and @filterInputBlur + @input.blur() - if @options.remote - @options.query search_text, (data) => + @options.query @input.val(), (data) => @options.callback(data) - else - @filter search_text - , 250 + , 250 + else + @filter @input.val() shouldBlur: (keyCode) -> return BLUR_KEYCODES.indexOf(keyCode) >= 0 @@ -142,6 +144,7 @@ class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + INDETERMINATE_CLASS = "is-indeterminate" currentIndex = -1 FILTER_INPUT = '.dropdown-input .dropdown-input-field' @@ -182,9 +185,6 @@ class GitLabDropdown @fullData = data @parseData @fullData - - if @options.filterable - @filterInput.trigger 'keyup' } # Init filterable @@ -298,6 +298,13 @@ class GitLabDropdown opened: => @addArrowKeyEvent() + if @options.setIndeterminateIds + @options.setIndeterminateIds.call(@) + + # Makes indeterminate items effective + if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + @parseData @fullData + contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() @@ -309,12 +316,18 @@ class GitLabDropdown hidden: (e) => @removeArrayKeyEvent() + + $input = @dropdown.find(".dropdown-input-field") + if @options.filterable - @dropdown - .find(".dropdown-input-field") + $input .blur() .val("") - .trigger("keyup") + + # Triggering 'keyup' will re-render the dropdown which is not always required + # specially if we want to keep the state of the dropdown needed for bulk-assignment + if not @options.persistWhenHide + $input.trigger("keyup") if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS @@ -358,7 +371,7 @@ class GitLabDropdown if @options.renderRow # Call the render function - html = @options.renderRow(data) + html = @options.renderRow.call(@options, data, @) else if not selected value = if @options.id then @options.id(data) else data.id @@ -443,6 +456,17 @@ class GitLabDropdown $(@el).find(".dropdown-toggle-text").text @options.toggleLabel else selectedObject + else if el.hasClass(INDETERMINATE_CLASS) + el.addClass ACTIVE_CLASS + el.removeClass INDETERMINATE_CLASS + + if not value? + field.remove() + + if not field.length and fieldName + @addInput(fieldName, value) + + return selectedObject else if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @@ -459,31 +483,42 @@ class GitLabDropdown $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) if value? if !field.length and fieldName - # Create hidden input for form - input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" - if @options.inputId? - input = $(input) - .attr('id', @options.inputId) - @dropdown.before input + @addInput(fieldName, value) else field.val value return selectedObject - selectRowAtIndex: (index) -> - selector = ".dropdown-content li:not(.divider):eq(#{index}) a" + addInput: (fieldName, value)-> + # Create hidden input for form + $input = $('<input>').attr('type', 'hidden') + .attr('name', fieldName) + .val(value) + + if @options.inputId? + $input.attr('id', @options.inputId) + + @dropdown.before $input + + selectRowAtIndex: (e, index) -> + selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" # simulate a click on the first link - $(selector, @dropdown).trigger "click" + $el = $(selector, @dropdown) + + if $el.length + e.preventDefault() + e.stopImmediatePropagation() + $(selector, @dropdown)[0].click() addArrowKeyEvent: -> ARROW_KEY_CODES = [38, 40] $input = @dropdown.find(".dropdown-input-field") - selector = '.dropdown-content li:not(.divider)' + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" @@ -511,8 +546,8 @@ class GitLabDropdown return false - if currentKeyCode is 13 - @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex + if currentKeyCode is 13 and currentIndex isnt -1 + @selectRowAtIndex e, currentIndex removeArrayKeyEvent: -> $('body').off 'keydown' diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee new file mode 100644 index 00000000000..e0f681acf0b --- /dev/null +++ b/app/assets/javascripts/graphs/application.js.coffee @@ -0,0 +1,7 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require_tree . diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee index f36c71fd25e..f36c71fd25e 100644 --- a/app/assets/javascripts/stat_graph.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph.js.coffee diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee index 3be14cb43dd..1d9fae7cf79 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee @@ -1,5 +1,4 @@ #= require d3 -#= require stat_graph_contributors_util class @ContributorsStatGraph init: (log) -> diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee index b7a0e073766..584d281a510 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee @@ -1,6 +1,4 @@ #= require d3 -#= require jquery -#= require underscore class @ContributorsGraph MARGIN: diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee index 31617c88b4a..31617c88b4a 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee new file mode 100644 index 00000000000..16d023dd391 --- /dev/null +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -0,0 +1,109 @@ +class @IssuableBulkActions + constructor: (opts = {}) -> + # Set defaults + { + @container = $('.content') + @form = @getElement('.bulk-update') + @issues = @getElement('.issues-list .issue') + } = opts + + @bindEvents() + + getElement: (selector) -> + @container.find selector + + bindEvents: -> + @form.off('submit').on('submit', @onFormSubmit.bind(@)) + + onFormSubmit: (e) -> + e.preventDefault() + @submit() + + submit: -> + _this = @ + + xhr = $.ajax + url: @form.attr 'action' + method: @form.attr 'method' + dataType: 'JSON', + data: @getFormDataAsObject() + + xhr.done (response, status, xhr) -> + location.reload() + + xhr.fail -> + new Flash("Issue update failed") + + xhr.always @onFormSubmitAlways.bind(@) + + onFormSubmitAlways: -> + @form.find('[type="submit"]').enable() + + getSelectedIssues: -> + @issues.has('.selected_issue:checked') + + getLabelsFromSelection: -> + labels = [] + + @getSelectedIssues().map -> + _labels = $(@).data('labels') + if _labels + _labels.map (labelId) -> + labels.push(labelId) if labels.indexOf(labelId) is -1 + + labels + + ###* + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + ### + getUnmarkedIndeterminedLabels: -> + result = [] + labelsToKeep = [] + + for el in @getElement('.labels-filter .is-indeterminate') + labelsToKeep.push $(el).data('labelId') + + for id in @getLabelsFromSelection() + # Only the ones that we are not going to keep + result.push(id) if labelsToKeep.indexOf(id) is -1 + + result + + ###* + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + ### + getFormDataAsObject: -> + formData = + update: + state_event : @form.find('input[name="update[state_event]"]').val() + assignee_id : @form.find('input[name="update[assignee_id]"]').val() + milestone_id : @form.find('input[name="update[milestone_id]"]').val() + issues_ids : @form.find('input[name="update[issues_ids]"]').val() + add_label_ids : [] + remove_label_ids : [] + + @getLabelsToApply().map (id) -> + formData.update.add_label_ids.push id + + @getLabelsToRemove().map (id) -> + formData.update.remove_label_ids.push id + + formData + + getLabelsToApply: -> + labelIds = [] + $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') + + $labels.each (k, label) -> + labelIds.push $(label).val() if label + + labelIds + + ###* + * Just an alias of @getUnmarkedIndeterminedLabels + * @return {Array} Array of labels + ### + getLabelsToRemove: -> + @getUnmarkedIndeterminedLabels() diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 995fd768603..ec74dfaae1a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -1,5 +1,7 @@ class @LabelsSelect constructor: -> + _this = @ + $('.js-label-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') @@ -196,10 +198,18 @@ class @LabelsSelect callback data - renderRow: (label) -> - removesAll = label.id is 0 or not label.id? + renderRow: (label, instance) -> + $li = $('<li>') + $a = $('<a href="#">') selectedClass = [] + removesAll = label.id is 0 or not label.id? + + if $dropdown.hasClass('js-filter-bulk-update') + indeterminate = instance.indeterminateIds + if indeterminate.indexOf(label.id) isnt -1 + selectedClass.push 'is-indeterminate' + if $form.find("input[type='hidden']\ [name='#{$dropdown.data('fieldName')}']\ [value='#{this.id(label)}']").length @@ -230,13 +240,17 @@ class @LabelsSelect else colorEl = '' - "<li> - <a href='#' class='#{selectedClass.join(' ')}'> - #{colorEl} - #{_.escape(label.title)} - </a> - </li>" - filterable: true + # We need to identify which items are actually labels + if label.id + selectedClass.push('label-item') + $a.attr('data-label-id', label.id) + + $a.addClass(selectedClass.join(' ')) + .html("#{colorEl} #{_.escape(label.title)}") + + # Return generated html + $li.html($a).prop('outerHTML') + persistWhenHide: $dropdown.data('persistWhenHide') search: fields: ['title'] selectable: true @@ -280,10 +294,19 @@ class @LabelsSelect else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else - saveLabelData() + if not $dropdown.hasClass 'js-filter-bulk-update' + saveLabelData() + + if $dropdown.hasClass('js-filter-bulk-update') + # If we are persisting state we need the classes + if not @options.persistWhenHide + $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> + if $dropdown.hasClass('js-filter-bulk-update') + return + page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is 'projects:merge_requests:index' @@ -298,4 +321,31 @@ class @LabelsSelect return else saveLabelData() + + setIndeterminateIds: -> + if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + @indeterminateIds = _this.getIndeterminateIds() ) + + @bindEvents() + + bindEvents: -> + $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue + + onSelectCheckboxIssue: -> + return if $('.selected_issue:checked').length + + # Remove inputs + $('.issues_bulk_update .labels-filter input[type="hidden"]').remove() + + # Also restore button text + $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label') + + getIndeterminateIds: -> + label_ids = [] + + $('.selected_issue:checked').each (i, el) -> + issue_id = $(el).data('id') + label_ids.push $("#issue_#{issue_id}").data('labels') + + _.flatten(label_ids) diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb new file mode 100644 index 00000000000..97be65116e2 --- /dev/null +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -0,0 +1,2 @@ +window.emojiAliases = -> + JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 345a0e447af..1d061d5edb7 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -83,7 +83,7 @@ class @MilestoneSelect $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (selected) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -118,7 +118,7 @@ class @MilestoneSelect $dropdown.trigger('loaded.gl.dropdown') $loading.fadeOut() $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') if data.milestone? data.milestone.namespace = _this.currentProject.namespace data.milestone.path = _this.currentProject.path diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index f8151963fa7..7c3d57fc194 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,7 +167,7 @@ class @Notes return if note.award - awardsHandler.addAwardToEmojiBar(note.note) + awardsHandler.addAwardToEmojiBar(note.name) awardsHandler.scrollToAwards() # render note if it not present in loaded list diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 6a7b4ad1db7..5eb915a51ea 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,8 +20,7 @@ class @SearchAutocomplete @dropdown = @wrap.find('.dropdown') @dropdownContent = @dropdown.find('.dropdown-content') - @locationBadgeEl = @getElement('.search-location-badge') - @locationText = @getElement('.location-text') + @locationBadgeEl = @getElement('.location-badge') @scopeInputEl = @getElement('#scope') @searchInput = @getElement('.search-input') @projectInputEl = @getElement('#search_project_id') @@ -133,7 +132,7 @@ class @SearchAutocomplete scope: @scopeInputEl.val() # Location badge - _location: @locationText.text() + _location: @locationBadgeEl.text() } bindEvents: -> @@ -143,23 +142,28 @@ class @SearchAutocomplete @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @clearInput.on 'click', @onClearInputClick + @locationBadgeEl.on 'click', => + @searchInput.focus() onDocumentClick: (e) => # If clicking outside the search box # And search input is not focused # And we are not clicking inside a suggestion - if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length @onSearchInputBlur() enableAutocomplete: -> # No need to enable anything if user is not logged in return if !gon.current_user_id - _this = @ - @loadingSuggestions = false + unless @dropdown.hasClass('open') + _this = @ + @loadingSuggestions = false - @dropdown.addClass('open') - @searchInput.removeClass('disabled') + @dropdown + .addClass('open') + .trigger('shown.bs.dropdown') + @searchInput.removeClass('disabled') onSearchInputKeyDown: => # Saves last length of the entered text @@ -190,7 +194,7 @@ class @SearchAutocomplete @disableAutocomplete() else # We should display the menu only when input is not empty - @enableAutocomplete() + @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER @wrap.toggleClass 'has-value', !!e.target.value @@ -221,10 +225,8 @@ class @SearchAutocomplete category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = "<span class='location-badge'> - <i class='location-text'>#{category}#{value}</i> - </span>" - @locationBadgeEl.html(html) + badgeText = "#{category}#{value}" + @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') restoreOriginalState: -> @@ -233,9 +235,8 @@ class @SearchAutocomplete for input in inputs @getElement("##{input}").val(@originalState[input]) - if @originalState._location is '' - @locationBadgeEl.empty() + @locationBadgeEl.hide() else @addLocationBadge( value: @originalState._location @@ -244,7 +245,7 @@ class @SearchAutocomplete @dropdown.removeClass 'open' badgePresent: -> - @locationBadgeEl.children().length + @locationBadgeEl.length resetSearchState: -> inputs = Object.keys @originalState @@ -257,7 +258,7 @@ class @SearchAutocomplete @getElement("##{input}").val('') removeLocationBadge: -> - @locationBadgeEl.empty() + @locationBadgeEl.hide() # Reset state @resetSearchState() diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee new file mode 100644 index 00000000000..6deb902c8de --- /dev/null +++ b/app/assets/javascripts/u2f/authenticate.js.coffee @@ -0,0 +1,63 @@ +# Authenticate U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> authenticated -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FAuthenticate + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @challenges = u2fParams.challenges + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + authenticate: () => + u2f.sign(@appId, @challenges, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderAuthenticated(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-authenticate-u2f-not-supported", + "setup": '#js-authenticate-u2f-setup', + "inProgress": '#js-authenticate-u2f-in-progress', + "error": '#js-authenticate-u2f-error', + "authenticated": '#js-authenticate-u2f-authenticated' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-login-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @authenticate() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderAuthenticated: (deviceResponse) => + @renderTemplate('authenticated') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee new file mode 100644 index 00000000000..1a2fc3e757f --- /dev/null +++ b/app/assets/javascripts/u2f/error.js.coffee @@ -0,0 +1,13 @@ +class @U2FError + constructor: (@errorCode) -> + @httpsDisabled = (window.location.protocol isnt 'https:') + console.error("U2F Error Code: #{@errorCode}") + + message: () => + switch + when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled) + "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." + when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE + "This device has already been registered with us." + else + "There was a problem communicating with your device." diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee new file mode 100644 index 00000000000..74472cfa120 --- /dev/null +++ b/app/assets/javascripts/u2f/register.js.coffee @@ -0,0 +1,63 @@ +# Register U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> registered -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FRegister + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @registerRequests = u2fParams.register_requests + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + register: () => + u2f.register(@appId, @registerRequests, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderRegistered(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-register-u2f-not-supported", + "setup": '#js-register-u2f-setup', + "inProgress": '#js-register-u2f-in-progress', + "error": '#js-register-u2f-error', + "registered": '#js-register-u2f-registered' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-setup-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @register() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderRegistered: (deviceResponse) => + @renderTemplate('registered') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb new file mode 100644 index 00000000000..d59341c38b9 --- /dev/null +++ b/app/assets/javascripts/u2f/util.js.coffee.erb @@ -0,0 +1,15 @@ +# Helper class for U2F (universal 2nd factor) device registration and authentication. + +class @U2FUtil + @isU2FSupported: -> + if @testMode + true + else + gon.u2f.browser_supports_u2f + + @enableTestMode: -> + @testMode = true + +<% if Rails.env.test? %> +U2FUtil.enableTestMode(); +<% end %> diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee new file mode 100644 index 00000000000..647ffbf5f45 --- /dev/null +++ b/app/assets/javascripts/users/application.js.coffee @@ -0,0 +1,8 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require d3 +#= require_tree . diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee index 26a26061539..26a26061539 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/users/calendar.js.coffee diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 519618aa617..de0eae58bff 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -149,7 +149,7 @@ class @UsersSelect hidden: (e) -> $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (user) -> page = $('body').data 'page' diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 93c63c69843..28634d0c59f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -232,9 +232,8 @@ a { padding-left: 25px; - &.is-active { + &.is-indeterminate, &.is-active { &::before { - content: "\f00c"; position: absolute; left: 5px; top: 50%; @@ -246,6 +245,14 @@ -moz-osx-font-smoothing: grayscale; } } + + &.is-indeterminate::before { + content: "\f068"; + } + + &.is-active::before { + content: "\f00c"; + } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 250d6309291..828e7224231 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -2,18 +2,10 @@ * Generic mixins */ @mixin box-shadow($shadow) { - -webkit-box-shadow: $shadow; - -moz-box-shadow: $shadow; - -ms-box-shadow: $shadow; - -o-box-shadow: $shadow; box-shadow: $shadow; } @mixin border-radius($radius) { - -webkit-border-radius: $radius; - -moz-border-radius: $radius; - -ms-border-radius: $radius; - -o-border-radius: $radius; border-radius: $radius; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8c54d935b6c..f253da814bc 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -63,7 +63,8 @@ $gl-padding-top: 10px; /* * Misc */ -$row-hover: #f4f8fe; +$row-hover: #f7faff; +$row-hover-border: #b2d7ff; $progress-color: #c0392b; $avatar_radius: 50%; $header-height: 50px; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 37bf38fa65d..07d40f40556 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,6 +1,4 @@ .awards { - line-height: 34px; - .emoji-icon { width: 20px; height: 20px; @@ -9,8 +7,6 @@ .emoji-menu { position: absolute; - top: 100%; - left: 0; margin-top: 3px; z-index: 1000; min-width: 160px; @@ -23,7 +19,12 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: all .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition-property: transform, opacity; + + &.is-aligned-right { + transform-origin: 100% -45px; + } &.is-visible { pointer-events: all; @@ -107,7 +108,7 @@ } &.is-loading { - .award-control-icon { + .award-control-icon-normal { display: none; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index aa41565f812..44222e8e8a4 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -3,12 +3,7 @@ background: #111; color: #fff; font-family: $monospace_font; - white-space: pre; - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ + white-space: pre-wrap; overflow: auto; overflow-y: hidden; font-size: 12px; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5e61e61d85c..1b389d83525 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,8 +29,6 @@ margin-top: 6px; p { - overflow-x: auto; - &:last-child { margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4f8a8748d3f..8046e203a99 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -41,7 +41,7 @@ margin: 0; margin-left: 20px; padding: 5px; - padding-top: 12px; + padding-top: 8px; line-height: 20px; &.right { @@ -110,6 +110,29 @@ p:last-child { margin-bottom: 0; } + + @media (max-width: $screen-sm-max) { + h4 { + font-size: 15px; + } + + p { + font-size: 13px; + } + + .btn, + .btn-group, + .accept-action { + width: 100%; + margin-bottom: 4px; + } + + .accept-control { + width: 100%; + text-align: center; + margin: 0; + } + } } .mr-widget-footer { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c64..ae524cd6bae 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,6 +28,7 @@ } .search-input { + padding-right: 20px; border: none; font-size: 14px; outline: none; @@ -47,6 +48,7 @@ display: inline-block; background-color: $location-badge-bg; vertical-align: top; + cursor: default; } .search-input-container { @@ -55,7 +57,7 @@ position: relative; } - .search-location-badge, .search-input-wrap { + .search-input-wrap { // Fallback if flexbox is not supported display: inline-block; } @@ -156,13 +158,11 @@ .search-holder { @media (min-width: $screen-sm-min) { display: -webkit-flex; - display: -ms-flexbox; display: flex; } .search-field-holder { -webkit-flex: 1 0 auto; - -ms-flex: 1 0 auto; flex: 1 0 auto; position: relative; margin-right: 0; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a84fc2e0318..f16fc7f388f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -15,16 +15,23 @@ margin-bottom: 0; tr { - > td, > th { + border-bottom: 1px solid $table-border-gray; + border-top: 1px solid $table-border-gray; + + td, th { line-height: 23px; } &:hover { + cursor: pointer; + td { - background: $row-hover; + background-color: $row-hover; + border-top: 1px solid $row-hover-border; + border-bottom: 1px solid $row-hover-border; } - cursor: pointer; } + &.selected { td { background: $gray-dark; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9b2a9d298b3..62f63701799 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base end def check_2fa_requirement - if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? - redirect_to new_profile_two_factor_auth_path + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor? + redirect_to profile_two_factor_auth_path end end @@ -232,7 +232,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } + devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email, :password, :login, :remember_me, :otp_attempt]) end def hexdigest(string) @@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end + def browser_supports_u2f? + browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + end + def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections @@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base current_user.nil? && root_path == request.path end + # U2F (universal 2nd factor) devices need a unique identifier for the application + # to perform authentication. + # https://developers.yubico.com/U2F/App_ID.html + def u2f_app_id + request.base_url + end + private def set_default_sort diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5918a7af3b..998b8adc411 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor # Returns nil def prompt_for_two_factor(user) session[:otp_user_id] = user.id + setup_u2f_authentication(user) + render 'devise/sessions/two_factor' + end + + def authenticate_with_two_factor + user = self.resource = find_user + + if user_params[:otp_attempt].present? && session[:otp_user_id] + authenticate_with_two_factor_via_otp(user) + elsif user_params[:device_response].present? && session[:otp_user_id] + authenticate_with_two_factor_via_u2f(user) + elsif user && user.valid_password?(user_params[:password]) + prompt_for_two_factor(user) + end + end + + private + + def authenticate_with_two_factor_via_otp(user) + if valid_otp_attempt?(user) + # Remove any lingering user data from login + session.delete(:otp_user_id) + + remember_me(user) if user_params[:remember_me] == '1' + sign_in(user) + else + flash.now[:alert] = 'Invalid two-factor code.' + render :two_factor + end + end + + # Authenticate using the response from a U2F (universal 2nd factor) device + def authenticate_with_two_factor_via_u2f(user) + if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges]) + # Remove any lingering user data from login + session.delete(:otp_user_id) + session.delete(:challenges) + + sign_in(user) + else + flash.now[:alert] = 'Authentication via U2F device failed.' + prompt_for_two_factor(user) + end + end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_authentication(user) + key_handles = user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) - render 'devise/sessions/two_factor' and return + if key_handles.present? + sign_requests = u2f.authentication_requests(key_handles) + challenges = sign_requests.map(&:challenge) + session[:challenges] = challenges + gon.push(u2f: { challenges: challenges, app_id: u2f_app_id, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb new file mode 100644 index 00000000000..09ff44f291b --- /dev/null +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -0,0 +1,22 @@ +module ToggleAwardEmoji + extend ActiveSupport::Concern + + included do + before_action :authenticate_user!, only: [:toggle_award_emoji] + end + + def toggle_award_emoji + name = params.require(:name) + + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(awardable, current_user) + + render json: { ok: true } + end + + private + + def awardable + raise NotImplementedError + end +end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 8f83fdd02bc..6a358fdcc05 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,7 +1,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_2fa_requirement - def new + def show unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) end @@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? - if two_factor_authentication_required? + if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' else grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." end end @qr_code = build_qr_code + setup_u2f_registration end def create if current_user.validate_and_consume_otp!(params[:pin_code]) - current_user.two_factor_enabled = true + current_user.otp_required_for_login = true @codes = current_user.generate_otp_backup_codes! current_user.save! @@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController else @error = 'Invalid pin code' @qr_code = build_qr_code + setup_u2f_registration + render 'show' + end + end + + # A U2F (universal 2nd factor) device's information is stored after successful + # registration, which is then used while 2FA authentication is taking place. + def create_u2f + @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges]) - render 'new' + if @u2f_registration.persisted? + session.delete(:challenges) + redirect_to profile_account_path, notice: "Your U2F device was registered!" + else + @qr_code = build_qr_code + setup_u2f_registration + render :show end end @@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def issuer_host Gitlab.config.gitlab.host end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_registration + @u2f_registration ||= U2fRegistration.new + @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) + + registration_requests = u2f.registration_requests + sign_requests = u2f.authentication_requests(@registration_key_handles) + session[:challenges] = registration_requests.map(&:challenge) + + gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, + register_requests: registration_requests, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index cfea1266516..832d7deb57d 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + @build ||= project.builds.find_by!(id: params[:build_id]) end def artifacts_file diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d09e7375b67..dd9508da049 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { render status: status[:return_code] } + format.js { render nothing: true, status: status[:return_code] } end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index bb1f6c5e980..db3ae586059 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:id]) + @build ||= project.builds.find_by!(id: params[:id]) end def build_path(build) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 016f5dd0005..4e2d3bebb2e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.nonawards.with_associations.fresh + @notes = @issue.notes.with_associations.fresh @noteable = @issue respond_to do |format| @@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController 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" }) + + respond_to do |format| + format.json do + render json: { notice: "#{result[:count]} issues updated" } + end + end end protected @@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController end alias_method :subscribable_resource, :issue alias_method :issuable, :issue + alias_method :awardable, :issue def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) @@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController :issues_ids, :assignee_id, :milestone_id, - :state_event + :state_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] ) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d54284d7b20..f78b429b3e7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleSubscriptionAction include DiffHelper include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :merge_request, only: [ @@ -190,13 +191,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end + if params[:sha] != @merge_request.source_sha + @status = :sha_mismatch + return + end + TodoService.new.merge_merge_request(merge_request, current_user) @merge_request.update(merge_error: nil) if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + .execute(@merge_request) @status = :merge_when_build_succeeds else MergeWorker.perform_async(@merge_request.id, current_user.id, params) @@ -265,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end alias_method :subscribable_resource, :merge_request alias_method :issuable, :merge_request + alias_method :awardable, :merge_request def closes_issues @closes_issues ||= @merge_request.closes_issues @@ -300,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh + @notes = @merge_request.mr_and_commit_notes.inc_author.fresh @discussions = @notes.discussions @noteable = @merge_request diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 40b24d550e0..c205474e999 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] + before_action :find_current_user_notes, only: [:index] def index current_fetched_at = Time.now.to_i @@ -56,30 +56,6 @@ class Projects::NotesController < Projects::ApplicationController end end - def award_toggle - noteable = if note_params[:noteable_type] == "issue" - project.issues.find(note_params[:noteable_id]) - else - project.merge_requests.find(note_params[:noteable_id]) - end - - data = { - author: current_user, - is_award: true, - note: note_params[:note].delete(":") - } - - note = noteable.notes.find_by(data) - - if note - note.destroy - else - Notes::CreateService.new(project, current_user, note_params).execute - end - - render json: { ok: true } - end - private def note @@ -131,13 +107,20 @@ class Projects::NotesController < Projects::ApplicationController end def note_json(note) - if note.valid? + if note.is_a?(AwardEmoji) + { + valid: note.valid?, + award: true, + id: note.id, + name: note.name + } + elsif note.valid? { valid: true, id: note.id, discussion_id: note.discussion_id, html: note_to_html(note), - award: note.is_award, + award: false, note: note.note, discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) @@ -145,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController else { valid: false, - award: note.is_award, + award: false, errors: note.errors } end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f94e2a84fa2..3af62c7696c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { - emojis: AwardEmoji.urls, + emojis: Gitlab::AwardEmoji.urls, issues: autocomplete.issues, milestones: autocomplete.milestones, mergerequests: autocomplete.merge_requests, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d68c2a708e3..f6eedb1773c 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end - authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" - log_audit_event(current_user, with: authenticated_with) + log_audit_event(current_user, with: authentication_method) end end @@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response) end def find_user @@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController find_user.try(:two_factor_enabled?) end - def authenticate_with_two_factor - user = self.resource = find_user - - if user_params[:otp_attempt].present? && session[:otp_user_id] - if valid_otp_attempt?(user) - # Remove any lingering user data from login - session.delete(:otp_user_id) - - remember_me(user) if user_params[:remember_me] == '1' - sign_in(user) and return - else - flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor and return - end - else - if user && user.valid_password?(user_params[:password]) - prompt_for_two_factor(user) - end - end - end - def auto_sign_in_with_provider provider = Gitlab.config.omniauth.auto_sign_in_with_provider return unless provider.present? @@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController def load_recaptcha Gitlab::Recaptcha.load_configurations! end + + def authentication_method + if user_params[:otp_attempt] + "two-factor" + elsif user_params[:device_response] + "two-factor-via-u2f-device" + else + "standard" + end + end end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index c41be333537..ee14ac60fb4 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,9 +12,9 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).non_diff_notes when "issue" - project.issues.find(target_id).notes.nonawards.inc_author + project.issues.find(target_id).notes.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 4bd46a76087..1d88116d7d2 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -30,7 +30,7 @@ class TodosFinder items = by_state(items) items = by_type(items) - items + items.reorder(id: :desc) end private @@ -78,6 +78,16 @@ class TodosFinder @project end + def projects + return @projects if defined?(@projects) + + if project? + @projects = project + else + @projects = ProjectsFinder.new.execute(current_user) + end + end + def type? type.present? && ['Issue', 'MergeRequest'].include?(type) end @@ -105,6 +115,8 @@ class TodosFinder def by_project(items) if project? items = items.where(project: project) + elsif projects + items = items.merge(projects).joins(:project) end items diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b05fa0a14d6..cd4d778e508 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -66,7 +66,7 @@ module AuthHelper def two_factor_skippable? current_application_settings.require_two_factor_authentication && - !current_user.two_factor_enabled && + !current_user.two_factor_enabled? && current_application_settings.two_factor_grace_period && !two_factor_grace_period_expired? end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index a9047ede8c5..f742922d926 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -30,7 +30,7 @@ module ButtonHelper content_tag :a, protocol, class: klass, - href: @project.http_url_to_repo, + href: project.http_url_to_repo, data: { html: true, placement: 'right', diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index b1f0a765bb9..4cac69c6795 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,7 +31,7 @@ module GroupsHelper if group && group.avatar.present? group.avatar.url else - 'no_group_avatar.png' + image_path('no_group_avatar.png') end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index fe84ee3de44..37b93f63145 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -96,5 +96,4 @@ module IssuablesHelper issuable.open? ? :opened : :closed end end - end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 173bdbb8654..72bd1fbbd81 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -145,16 +145,14 @@ module IssuesHelper end end - def emoji_author_list(notes, current_user) - list = notes.map do |note| - note.author == current_user ? "me" : note.author.name - end - - list.join(", ") + def award_user_list(awards, current_user) + awards.map do |award| + award.user == current_user ? 'me' : award.user.name + end.join(', ') end - def note_active_class(notes, current_user) - if current_user && notes.pluck(:author_id).include?(current_user.id) + def award_active_class(awards, current_user) + if current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb new file mode 100644 index 00000000000..91dd91718dc --- /dev/null +++ b/app/helpers/javascript_helper.rb @@ -0,0 +1,7 @@ +module JavascriptHelper + def page_specific_javascripts(js = nil) + @page_specific_javascripts = js unless js.nil? + + @page_specific_javascripts + end +end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b9d7edb4185..b4923fbb138 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -17,7 +17,9 @@ module TodosHelper def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), + class: 'has-tooltip', + title: todo.target.title end def todo_target_path(todo) diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb new file mode 100644 index 00000000000..59c7d87f5df --- /dev/null +++ b/app/models/award_emoji.rb @@ -0,0 +1,26 @@ +class AwardEmoji < ActiveRecord::Base + DOWNVOTE_NAME = "thumbsdown".freeze + UPVOTE_NAME = "thumbsup".freeze + + include Participable + + belongs_to :awardable, polymorphic: true + belongs_to :user + + validates :awardable, :user, presence: true + validates :name, presence: true, inclusion: { in: Emoji.emojis_names } + validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] } + + participant :user + + scope :downvotes, -> { where(name: DOWNVOTE_NAME) } + scope :upvotes, -> { where(name: UPVOTE_NAME) } + + def downvote? + self.name == DOWNVOTE_NAME + end + + def upvote? + self.name == UPVOTE_NAME + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5e77fda70b9..64723ab6b4b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -313,6 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + project.running_or_pending_build_count(force: true) end def artifacts? diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 10802f64813..f8d5d4486fd 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -11,6 +11,9 @@ module Ci format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } - attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' end end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb new file mode 100644 index 00000000000..aa4b4201250 --- /dev/null +++ b/app/models/concerns/awardable.rb @@ -0,0 +1,81 @@ +module Awardable + extend ActiveSupport::Concern + + included do + has_many :award_emoji, as: :awardable, dependent: :destroy + + if self < Participable + participant :award_emoji + end + end + + module ClassMethods + def order_upvotes_desc + order_votes_desc(AwardEmoji::UPVOTE_NAME) + end + + def order_downvotes_desc + order_votes_desc(AwardEmoji::DOWNVOTE_NAME) + end + + def order_votes_desc(emoji_name) + awardable_table = self.arel_table + awards_table = AwardEmoji.arel_table + + join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on( + awards_table[:awardable_id].eq(awardable_table[:id]).and( + awards_table[:awardable_type].eq(self.name).and( + awards_table[:name].eq(emoji_name) + ) + ) + ).join_sources + + joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC") + end + end + + def grouped_awards(with_thumbs: true) + awards = award_emoji.group_by(&:name) + + if with_thumbs + awards[AwardEmoji::UPVOTE_NAME] ||= [] + awards[AwardEmoji::DOWNVOTE_NAME] ||= [] + end + + awards + end + + def downvotes + award_emoji.downvotes.count + end + + def upvotes + award_emoji.upvotes.count + end + + def emoji_awardable? + true + end + + def awarded_emoji?(emoji_name, current_user) + award_emoji.where(name: emoji_name, user: current_user).exists? + end + + def create_award_emoji(name, current_user) + return unless emoji_awardable? + + award_emoji.create(name: name, user: current_user) + end + + def remove_award_emoji(name, current_user) + award_emoji.where(name: name, user: current_user).destroy_all + end + + def toggle_award_emoji(emoji_name, current_user) + if awarded_emoji?(emoji_name, current_user) + remove_award_emoji(emoji_name, current_user) + else + create_award_emoji(emoji_name, current_user) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2326a395cb8..5d279ae602a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -10,6 +10,7 @@ module Issuable include Mentionable include Subscribable include StripAttribute + include Awardable included do belongs_to :author, class_name: "User" @@ -68,6 +69,14 @@ module Issuable strip_attributes :title acts_as_paranoid + + after_save :update_assignee_cache_counts, if: :assignee_id_changed? + + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignee + User.find(assignee_id_was).update_cache_counts if assignee_id_was + assignee.update_cache_counts if assignee + end end module ClassMethods @@ -107,29 +116,6 @@ module Issuable end end - def order_downvotes_desc - order_votes_desc('thumbsdown') - end - - def order_upvotes_desc - order_votes_desc('thumbsup') - end - - def order_votes_desc(award_emoji_name) - issuable_table = self.arel_table - note_table = Note.arel_table - - join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on( - note_table[:noteable_id].eq(issuable_table[:id]).and( - note_table[:noteable_type].eq(self.name).and( - note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name)) - ) - ) - ).join_sources - - joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") - end - def with_label(title, sort = nil) if title.is_a?(Array) && title.size > 1 joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") @@ -163,10 +149,6 @@ module Issuable today? && created_at == updated_at end - def is_assigned? - !!assignee_id - end - def is_being_reassigned? assignee_id_changed? end @@ -175,14 +157,6 @@ module Issuable opened? || reopened? end - def downvotes - notes.awards.where(note: "thumbsdown").count - end - - def upvotes - notes.awards.where(note: "thumbsup").count - end - def user_notes_count notes.user.count end @@ -205,6 +179,10 @@ module Issuable hook_data end + def labels_array + labels.to_a + end + def label_names labels.order('title ASC').pluck(:title) end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index bbefc911b29..95fd510eb3a 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -110,6 +110,10 @@ class LegacyDiffNote < Note @active end + def award_emoji_supported? + false + end + private def find_diff diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 1ac37e0307f..a2aee2f925b 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -164,7 +164,7 @@ module Network i != range.last && @commits[i].spaces.include?(overlap_space) - return true; + return true end end @@ -205,7 +205,7 @@ module Network # Visit branching chains leaves.each do |l| parents = l.parents(@map).select{|p| p.space.zero?} - for p in parents + parents.each do |p| place_chain(p, l.time) end end @@ -223,7 +223,7 @@ module Network end def mark_reserved(time_range, space) - for day in time_range + time_range.each do |day| @reserved[day].push(space) end end @@ -232,7 +232,7 @@ module Network space_default ||= space_base reserved = [] - for day in time_range + time_range.each do |day| reserved.push(*@reserved[day]) end reserved.uniq! diff --git a/app/models/note.rb b/app/models/note.rb index c21981ead84..46c3f6e24af 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -21,11 +21,8 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true delegate :title, to: :noteable, allow_nil: true - before_validation :set_award! - validates :note, :project, presence: true - validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } - validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } + # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -43,8 +40,6 @@ class Note < ActiveRecord::Base mount_uploader :attachment, AttachmentUploader # Scopes - scope :awards, ->{ where(is_award: true) } - scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } @@ -109,19 +104,6 @@ class Note < ActiveRecord::Base found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') end end - - def grouped_awards - notes = {} - - awards.select(:note).distinct.map do |note| - notes[note.note] = where(note: note.note) - end - - notes["thumbsup"] ||= Note.none - notes["thumbsdown"] ||= Note.none - - notes - end end def cross_reference? @@ -205,44 +187,24 @@ class Note < ActiveRecord::Base Event.reset_event_cache_for(self) end - def downvote? - is_award && note == "thumbsdown" - end - - def upvote? - is_award && note == "thumbsup" - end - def editable? - !system? && !is_award + !system? end def cross_reference_not_visible_for?(user) cross_reference? && referenced_mentionables(user).empty? end - # Checks if note is an award added as a comment - # - # If note is an award, this method sets is_award to true - # and changes content of the note to award name. - # - # Method is executed as a before_validation callback. - # - def set_award! - return unless awards_supported? && contains_emoji_only? - - self.is_award = true - self.note = award_emoji_name + def award_emoji? + award_emoji_supported? && contains_emoji_only? end - private - def clear_blank_line_code! self.line_code = nil if self.line_code.blank? end - def awards_supported? - (for_issue? || for_merge_request?) && !diff_note? + def award_emoji_supported? + noteable.is_a?(Awardable) end def contains_emoji_only? @@ -251,6 +213,6 @@ class Note < ActiveRecord::Base def award_emoji_name original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - AwardEmoji.normilize_emoji_name(original_name) + Gitlab::AwardEmoji.normalize_emoji_name(original_name) end end diff --git a/app/models/project.rb b/app/models/project.rb index c1d9bae44c9..e4a9d17a20c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -309,21 +309,25 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def container_registry_path_with_namespace + path_with_namespace.downcase + end + def container_registry_repository return unless Gitlab.config.registry.enabled @container_registry_repository ||= begin - token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace) + token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) url = Gitlab.config.registry.api_url host_port = Gitlab.config.registry.host_port registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) - registry.repository(path_with_namespace) + registry.repository(container_registry_path_with_namespace) end end def container_registry_repository_url if Gitlab.config.registry.enabled - "#{Gitlab.config.registry.host_port}/#{path_with_namespace}" + "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" end end @@ -1007,4 +1011,22 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end + + def running_or_pending_build_count(force: false) + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do + builds.running_or_pending.count(:all) + end + end + + def mark_import_as_failed(error_message) + original_errors = errors.dup + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + + import_fail + update_column(:import_error, sanitized_message) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + ensure + @errors = original_errors + end end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index e2f9ffb69ac..ca8a9b4217b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -6,7 +6,8 @@ class ProjectImportData < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, - mode: :per_attribute_iv_and_salt + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' serialize :data, JSON diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 2e5e854fc5e..58cb720c3c1 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -83,7 +83,7 @@ class IrkerService < Service self.channels = recipients.split(/\s+/).map do |recipient| format_channel(recipient) end - channels.reject! &:nil? + channels.reject!(&:nil?) end def format_channel(recipient) diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb new file mode 100644 index 00000000000..00b19686d48 --- /dev/null +++ b/app/models/u2f_registration.rb @@ -0,0 +1,40 @@ +# Registration information for U2F (universal 2nd factor) devices, like Yubikeys + +class U2fRegistration < ActiveRecord::Base + belongs_to :user + + def self.register(user, app_id, json_response, challenges) + u2f = U2F::U2F.new(app_id) + registration = self.new + + begin + response = U2F::RegisterResponse.load_from_json(json_response) + registration_data = u2f.register!(challenges, response) + registration.update(certificate: registration_data.certificate, + key_handle: registration_data.key_handle, + public_key: registration_data.public_key, + counter: registration_data.counter, + user: user) + rescue JSON::ParserError, NoMethodError, ArgumentError + registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') + rescue U2F::Error => e + registration.errors.add(:base, e.message) + end + + registration + end + + def self.authenticate(user, app_id, json_response, challenges) + response = U2F::SignResponse.load_from_json(json_response) + registration = user.u2f_registrations.find_by_key_handle(response.key_handle) + u2f = U2F::U2F.new(app_id) + + if registration + u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter) + registration.update(counter: response.counter) + true + end + rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error + false + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 6a09b78455b..e0987e07e1f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,14 +20,18 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme + attr_encrypted :otp_secret, + key: Gitlab::Application.config.secret_key_base, + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' + devise :two_factor_authenticatable, otp_secret_encryption_key: Gitlab::Application.config.secret_key_base - alias_attribute :two_factor_enabled, :otp_required_for_login devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON - devise :lockable, :async, :recoverable, :rememberable, :trackable, + devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -46,6 +50,7 @@ class User < ActiveRecord::Base has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true + has_many :u2f_registrations, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -79,6 +84,7 @@ class User < ActiveRecord::Base has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy + has_many :award_emoji, as: :awardable, dependent: :destroy # # Validations @@ -169,8 +175,16 @@ class User < ActiveRecord::Base scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } - scope :with_two_factor, -> { where(two_factor_enabled: true) } - scope :without_two_factor, -> { where(two_factor_enabled: false) } + + def self.with_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + end + + def self.without_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NULL AND otp_required_for_login = ?", false) + end # # Class methods @@ -317,14 +331,29 @@ class User < ActiveRecord::Base end def disable_two_factor! - update_attributes( - two_factor_enabled: false, - encrypted_otp_secret: nil, - encrypted_otp_secret_iv: nil, - encrypted_otp_secret_salt: nil, - otp_grace_period_started_at: nil, - otp_backup_codes: nil - ) + transaction do + update_attributes( + otp_required_for_login: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_grace_period_started_at: nil, + otp_backup_codes: nil + ) + self.u2f_registrations.destroy_all + end + end + + def two_factor_enabled? + two_factor_otp_enabled? || two_factor_u2f_enabled? + end + + def two_factor_otp_enabled? + self.otp_required_for_login? + end + + def two_factor_u2f_enabled? + self.u2f_registrations.exists? end def namespace_uniq @@ -771,6 +800,23 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + def assigned_open_merge_request_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do + assigned_merge_requests.opened.count + end + end + + def assigned_open_issues_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do + assigned_issues.opened.count + end + end + + def update_cache_counts + assigned_open_merge_request_count(force: true) + assigned_open_issues_count(force: true) + end + private def projects_union diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2b16089df1b..e3dc569152c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,8 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:add_label_ids) + params.delete(:remove_label_ids) params.delete(:label_ids) params.delete(:assignee_id) end @@ -67,10 +69,34 @@ class IssuableBaseService < BaseService end def filter_labels - return if params[:label_ids].to_a.empty? + if params[:add_label_ids].present? || params[:remove_label_ids].present? + params.delete(:label_ids) + + filter_labels_in_param(:add_label_ids) + filter_labels_in_param(:remove_label_ids) + else + filter_labels_in_param(:label_ids) + end + end + + def filter_labels_in_param(key) + return if params[key].to_a.empty? - params[:label_ids] = - project.labels.where(id: params[:label_ids]).pluck(:id) + params[key] = project.labels.where(id: params[key]).pluck(:id) + end + + def update_issuable(issuable, attributes) + issuable.with_transaction_returning_status do + add_label_ids = attributes.delete(:add_label_ids) + remove_label_ids = attributes.delete(:remove_label_ids) + + issuable.label_ids |= add_label_ids if add_label_ids + issuable.label_ids -= remove_label_ids if remove_label_ids + + issuable.assign_attributes(attributes.merge(updated_by: current_user)) + + issuable.save + end end def update(issuable) @@ -78,7 +104,7 @@ class IssuableBaseService < BaseService filter_params old_labels = issuable.labels.to_a - if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) + if params.present? && update_issuable(issuable, params) issuable.reset_events_cache handle_common_system_notes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index de8387c4900..15825b81685 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -4,9 +4,9 @@ module Issues issues_ids = params.delete(:issues_ids).split(",") issue_params = params - issue_params.delete(:state_event) unless issue_params[:state_event].present? - issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? - issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? + %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key| + issue_params.delete(key) unless issue_params[key].present? + end issues = Issue.where(id: issues_ids) issues.each do |issue| diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index e61628086f0..ab667456db7 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -24,6 +24,7 @@ module Issues @new_issue = create_new_issue rewrite_notes + rewrite_award_emoji add_note_moved_from # Old issue tasks @@ -72,6 +73,14 @@ module Issues end end + def rewrite_award_emoji + @old_issue.award_emoji.each do |award| + new_award = award.dup + new_award.awardable = @new_issue + new_award.save + end + end + def rewrite_content(content) return unless content diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2bb312bb252..02fca5c0ea3 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,6 +5,13 @@ module Notes note.author = current_user note.system = false + if note.award_emoji? + noteable = note.noteable + todo_service.new_award_emoji(noteable, current_user) + + return noteable.create_award_emoji(note.award_emoji_name, current_user) + end + if note.save # Finish the harder work in the background NewNoteWorker.perform_in(2.seconds, note.id, params) diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index e818f58d13c..534c48aefff 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -8,7 +8,7 @@ module Notes def execute # Skip system notes, like status changes and cross-references and awards - unless @note.system || @note.is_award + unless @note.system? EventCreateService.new.leave_note(@note, @note.author) @note.create_cross_references! execute_note_hooks diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 42ec1ac9e1a..91ca82ed3b7 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -130,8 +130,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') - return true if note.cross_reference? && note.system == true - return true if note.is_award + return true if note.cross_reference? && note.system? target = note.noteable diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb index 6194f6ce91e..264fdccde8f 100644 --- a/app/services/oauth2/access_token_validation_service.rb +++ b/app/services/oauth2/access_token_validation_service.rb @@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService end protected + # True if the token's scope is a superset of required scopes, # or the required scopes is empty. def sufficient_scope?(token, scopes) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6728fabea1e..61cac5419ad 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -56,14 +56,14 @@ module Projects after_create_actions if @project.persisted? - @project.add_import_job if @project.import? - + if @project.errors.empty? + @project.add_import_job if @project.import? + else + fail(error: @project.errors.full_messages.join(', ')) + end @project rescue => e - message = "Unable to save project: #{e.message}" - Rails.logger.error(message) - @project.errors.add(:base, message) if @project - @project + fail(error: e.message) end protected @@ -103,5 +103,19 @@ module Projects end end end + + def fail(error:) + message = "Unable to save project. Error: #{error}" + message << "Project ID: #{@project.id}" if @project && @project.id + + Rails.logger.error(message) + + if @project && @project.import? + @project.errors.add(:base, message) + @project.mark_import_as_failed(message) + end + + @project + end end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index ef15ef6a473..c4838d31f2f 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -39,7 +39,7 @@ module Projects begin gitlab_shell.import_repository(project.path_with_namespace, project.import_url) rescue Gitlab::Shell::Error => e - raise Error, e.message + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 4bf4e144727..d8365124175 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -122,6 +122,14 @@ class TodoService handle_note(note, current_user) end + # When an emoji is awarded we should: + # + # * mark all pending todos related to the awardable for the current user as done + # + def new_award_emoji(awardable, current_user) + mark_pending_todos_as_done(awardable, current_user) + end + # When marking pending todos as done we should: # # * mark all pending todos related to the target for the current user as done diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml new file mode 100644 index 00000000000..e9302c39753 --- /dev/null +++ b/app/views/award_emoji/_awards_block.html.haml @@ -0,0 +1,18 @@ +- grouped_emojis = awardable.grouped_awards(with_thumbs: inline) +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } + - awards_sort(grouped_emojis).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } + = emoji_icon(emoji, sprite: false) + %span.award-control-text.js-counter + = awards.count + + - if current_user + :javascript + gl.awardMenuUrl = "#{emojis_path}" + + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{ type: "button", data: { award_menu_url: emojis_path } } + = icon('smile-o', class: "award-control-icon award-control-icon-normal") + = icon('spinner spin', class: "award-control-icon award-control-icon-loading") + %span.award-control-text + Add diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 8c6a1552a53..9d04db2c45e 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,11 +1,18 @@ %div .login-box .login-heading - %h3 Two-factor Authentication + %h3 Two-Factor Authentication .login-body - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.hidden_field :remember_me, value: params[resource_name][:remember_me] - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. - .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + - if @user.two_factor_otp_enabled? + %h5 Authenticate via Two-Factor App + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + = f.hidden_field :remember_me, value: params[resource_name][:remember_me] + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" + + - if @user.two_factor_u2f_enabled? + + %hr + = render "u2f/authenticate" diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index 3443a8e2307..97401a2e618 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -1,9 +1,9 @@ .emoji-menu .emoji-menu-content = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" - - AwardEmoji.emoji_by_category.each do |category, emojis| + - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis| %h5.emoji-menu-title - = AwardEmoji::CATEGORIES[category] + = Gitlab::AwardEmoji::CATEGORIES[category] %ul.clearfix.emoji-menu-list - emojis.each do |emoji| %li.pull-left.text-center.emoji-menu-list-item diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 70e88da7aae..01648047ce2 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -24,7 +24,7 @@ %td Show/hide this dialog %tr %td.shortcut - - if browser.mac? + - if browser.platform.mac? .key ⌘ shift p - else .key ctrl shift p diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 79cdbac1f37..e0ed657919e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,9 +30,10 @@ = javascript_include_tag "application" - = csrf_meta_tags + - if page_specific_javascripts + = javascript_include_tag page_specific_javascripts, {"data-turbolinks-track" => true} - = include_gon + = csrf_meta_tags - unless browser.safari? %meta{name: 'referrer', content: 'origin-when-cross-origin'} diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 6b208c3d0bb..b49207fc315 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,11 +6,8 @@ .search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - .search-location-badge - - if label.present? - %span.location-badge - %i.location-text - = label + - if label.present? + .location-badge= label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e4d1c773d03..2b86b289bbe 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,6 +2,8 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + = Gon::Base.render_data + -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. = yield :scripts_body_top diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index f08cb0a5428..3d28eec84ef 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 7c061dd531f..6bd427b02ac 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 915acc4612e..7fbe065df00 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme} application navless"} + = Gon::Base.render_data = render "layouts/header/empty" .container.navless-container = render "layouts/flash" diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 43532b0c155..306ebd5fcf7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -30,13 +30,13 @@ = icon('exclamation-circle fw') %span Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_issues_count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_merge_request_count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 087b7472701..03c9fa0a94d 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -33,18 +33,11 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') + = icon('code fw') %span - Files - - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + Code - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do @@ -52,22 +45,13 @@ = icon('ship fw') %span Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) - - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span - Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - if project_nav_tab? :container_registry = nav_link(controller: %w(container_registry)) do = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do = icon('hdd-o fw') %span - Container Registry + Registry - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do @@ -132,4 +116,16 @@ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do Create a new issue + -# Shortcut to builds page + - if project_nav_tab? :builds + %li.hidden + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + Builds + + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + .fade-right diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 01ac8161945..3d2a245ecbd 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -11,7 +11,7 @@ %p Your private token is used to access application resources without authentication. .col-lg-9 - = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| + = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f| %p.cgray - if current_user.private_token = label_tag "token", "Private token", class: "label-light" @@ -29,21 +29,22 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Two-factor Authentication + Two-Factor Authentication %p - Increase your account's security by enabling two-factor authentication (2FA). + Increase your account's security by enabling Two-Factor Authentication (2FA). .col-lg-9 %p - Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} - - if !current_user.two_factor_enabled? - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .append-bottom-10 - = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' + Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + - if current_user.two_factor_enabled? + = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to 'Disable', profile_two_factor_auth_path, + method: :delete, + data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." }, + class: 'btn btn-danger' - else - = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', - data: { confirm: 'Are you sure?' } + .append-bottom-10 + = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success' + %hr - if button_based_providers.any? .row.prepend-top-default diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml deleted file mode 100644 index 69fc81cb45c..00000000000 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -- page_title 'Two-factor Authentication', 'Account' - -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - Two-factor Authentication (2FA) - %p - Increase your account's security by enabling two-factor authentication (2FA). - .col-lg-9 - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .row.append-bottom-10 - .col-md-3 - = raw @qr_code - .col-md-9 - .account-well - %p.prepend-top-0.append-bottom-0 - Can't scan the code? - %p.prepend-top-0.append-bottom-0 - To add the entry manually, provide the following details to the application on your phone. - %p.prepend-top-0.append-bottom-0 - Account: - = current_user.email - %p.prepend-top-0.append-bottom-0 - Key: - = current_user.otp_secret.scan(/.{4}/).join(' ') - %p.two-factor-new-manual-content - Time based: Yes - = form_tag profile_two_factor_auth_path, method: :post do |f| - - if @error - .alert.alert-danger - = @error - .form-group - = label_tag :pin_code, nil, class: "label-light" - = text_field_tag :pin_code, nil, class: "form-control", required: true - .prepend-top-default - = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' - = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml new file mode 100644 index 00000000000..ce76cb73c9c --- /dev/null +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -0,0 +1,69 @@ +- page_title 'Two-Factor Authentication', 'Account' +- header_title "Two-Factor Authentication", profile_two_factor_auth_path + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Register Two-Factor Authentication App + %p + Use an app on your mobile device to enable two-factor authentication (2FA). + .col-lg-9 + - if current_user.two_factor_otp_enabled? + = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." + - else + %p + Download the Google Authenticator application from App Store or Google Play Store and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-3 + = raw @qr_code + .col-md-9 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = current_user.email + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_tag profile_two_factor_auth_path, method: :post do |f| + - if @error + .alert.alert-danger + = @error + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Register with Two-Factor App', class: 'btn btn-success' + +%hr + +.row.prepend-top-default + + .col-lg-3 + %h4.prepend-top-0 + Register Universal Two-Factor (U2F) Device + %p + Use a hardware device to add the second factor of authentication. + %p + As U2F devices are only supported by a few browsers, it's recommended that you set up a + two-factor authentication app as well as a U2F device so you'll always be able to log in + using an unsupported browser. + .col-lg-9 + %p + - if @registration_key_handles.present? + = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab." + - if @u2f_registration.errors.present? + = form_errors(@u2f_registration) + = render "u2f/register" + +- if two_factor_skippable? + :javascript + var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; + $(".flash-alert").append(button); + diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml deleted file mode 100644 index a21ddaf4930..00000000000 --- a/app/views/projects/branches/destroy.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 8fb9ebc1b8b..818d5d28f04 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,4 +1,5 @@ - page_title "Builds" += render "projects/pipelines/head" .top-area %ul.nav-links diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index d1bd76ab529..1c136133ab0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,9 +1,11 @@ %ul.nav-links + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files + = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - %span.badge - = number_with_delimiter(@repository.commit_count) = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do @@ -16,9 +18,7 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tag_count diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 79a56647c53..8becaea246f 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,3 +1,4 @@ +- page_specific_javascripts asset_path("graphs/application.js") %ul.nav-links = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 78f64150601..79b14819865 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" @@ -27,7 +27,7 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.nonawards.count + - note_count = issue.notes.user.count %li = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do = icon('comments') diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f3b0469b7d4..a35c13fbd40 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -68,9 +68,9 @@ #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' - = render 'votes/votes_block', votable: @issue + .content-block.content-block-small + = render 'new_branch' + = render 'award_emoji/awards_block', awardable: @issue, inline: true %section.issuable-discussion = render 'projects/issues/discussion' diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 8bf544b8371..294fec422c5 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,6 +1,5 @@ -%li{id: dom_id(label)} +%li{ id: dom_id(label), data: { id: label.id } } = render "shared/label_row", label: label - .pull-info-right %span.append-right-20 = link_to_label(label, type: :merge_request) do diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c02f94490a0..1ec180235ce 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -35,7 +35,7 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.nonawards.count + - note_count = merge_request.mr_and_commit_notes.user.count %li = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do = icon('comments') diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 5473fa19166..446887774a4 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -6,4 +6,3 @@ - if @merge_requests.present? = paginate @merge_requests, theme: "gitlab" - diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7af227129ec..a73d0063be2 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -49,7 +49,7 @@ %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count + %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits @@ -67,7 +67,7 @@ .tab-content #notes.notes.tab-pane.voting_notes .content-block.content-block-small.oneline-block - = render 'votes/votes_block', votable: @merge_request + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .row %section.col-md-12 diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml index ba8d9a5835c..a60c445aa51 100644 --- a/app/views/projects/merge_requests/dropdowns/_branch.html.haml +++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml @@ -1,5 +1,5 @@ %ul - branches.each do |branch| %li - %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } } + %a{ href: '#', class: "#{('is-active' if selected == branch)}", title: branch, data: { id: branch } } = branch diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 92ce479d463..84b6c9ebc5c 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -5,6 +5,9 @@ - when :merge_when_build_succeeds :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); +- when :sha_mismatch + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index cfdf4edac37..0d49b6471a9 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -2,6 +2,7 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token + = hidden_field_tag :sha, @merge_request.source_sha .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index b83ddcab3a4..ad898ff153b 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -16,7 +16,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml new file mode 100644 index 00000000000..499624f8dd8 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon("exclamation-triangle") + This merge request has received new commits since the page was loaded. + +%p + Please reload the page to review the new commits before merging. diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml new file mode 100644 index 00000000000..6e757df5417 --- /dev/null +++ b/app/views/projects/pipelines/_head.html.haml @@ -0,0 +1,14 @@ +%ul.nav-links + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count) + + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 8788db09dbe..453767920b5 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -1,4 +1,5 @@ - page_title "Pipelines" += render "projects/pipelines/head" .top-area %ul.nav-links diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index ffeacb5a004..e4a78fadbeb 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,3 +1,2 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}"); - if @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 7e9ba09c720..59f60c4687c 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,6 +3,7 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' += render "projects/commits/head" .tree-controls = render 'projects/find_file_link' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index cedff4af2e0..380ab465bf4 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -31,7 +31,7 @@ - if controller.controller_name == 'issues' .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do .filter-item.inline = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul @@ -44,6 +44,10 @@ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 61fd1e9c335..d34d28f6736 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,14 +1,25 @@ +- show_create = local_assigns.fetch(:show_create, true) +- extra_options = local_assigns.fetch(:extra_options, true) +- filter_submit = local_assigns.fetch(:filter_submit, true) +- show_footer = local_assigns.fetch(:show_footer, true) +- data_options = local_assigns.fetch(:data_options, {}) +- classes = local_assigns.fetch(:classes, []) +- dropdown_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"} +- dropdown_data.merge!(data_options) +- classes << 'js-extra-options' if extra_options +- classes << 'js-filter-submit' if filter_submit + - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.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"}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + - if show_create and @project and can?(current_user, :admin_label, @project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 7f4867417f7..4e280c371ac 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,20 +1,22 @@ - title = local_assigns.fetch(:title, 'Assign labels') +- show_create = local_assigns.fetch(:show_create, true) +- show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one = dropdown_title(title) = dropdown_filter(filter_placeholder) = dropdown_content - - if @project + - if @project && show_footer = dropdown_footer do %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) %li %a.dropdown-toggle-page{href: "#"} Create new %li = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do - - if can? current_user, :admin_label, @project + - if show_create && @project && can?(current_user, :admin_label, @project) Manage labels - else View labels - = dropdown_loading
\ No newline at end of file + = dropdown_loading diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1eec450193..d6552ae7f18 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -114,20 +114,20 @@ .sidebar-collapsed-icon = icon('tags') %span - = issuable.labels.count + = issuable.labels_array.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - 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? - - issuable.labels.each do |label| + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + - if issuable.labels_array.any? + - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - - issuable.labels.each do |label| + - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml new file mode 100644 index 00000000000..75fb0e303ad --- /dev/null +++ b/app/views/u2f/_authenticate.html.haml @@ -0,0 +1,28 @@ +#js-authenticate-u2f + +%script#js-authenticate-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-authenticate-u2f-setup{ type: "text/template" } + %div + %p Insert your security key (if you haven't already), and press the button below. + %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device + +%script#js-authenticate-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-authenticate-u2f-error{ type: "text/template" } + %div + %p <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-authenticate-u2f-authenticated{ type: "text/template" } + %div + %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. + = form_tag(new_user_session_path, method: :post) do |f| + = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Authenticate via U2F Device", class: "btn btn-success" + +:javascript + var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f); + u2fAuthenticate.start(); diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml new file mode 100644 index 00000000000..46af591fc43 --- /dev/null +++ b/app/views/u2f/_register.html.haml @@ -0,0 +1,31 @@ +#js-register-u2f + +%script#js-register-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-register-u2f-setup{ type: "text/template" } + .row.append-bottom-10 + .col-md-3 + %a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device + .col-md-9 + %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. + +%script#js-register-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-register-u2f-error{ type: "text/template" } + %div + %p + %span <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-register-u2f-registered{ type: "text/template" } + %div.row.append-bottom-10 + %p Your device was successfully set up! Click this button to register with the GitLab server. + = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do + = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Register U2F Device", class: "btn btn-success" + +:javascript + var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f); + u2fRegister.start(); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0c513308308..8268380dafc 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,6 @@ - page_title @user.name - page_description @user.bio +- page_specific_javascripts asset_path("users/application.js") - header_title @user.name, user_path(@user) - @no_container = true diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml deleted file mode 100644 index 4beb8746444..00000000000 --- a/app/views/votes/_votes_block.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -.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)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} - = emoji_icon(emoji, sprite: false) - %span.award-control-text.js-counter - = notes.count - - - if current_user - %div.award-menu-holder.js-award-holder - %a.btn.award-control.js-add-award{"href" => "#"} - = icon('smile-o', {class: "award-control-icon"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - %span.award-control-text - Add - -- if current_user - :javascript - var getEmojisUrl = "#{emojis_path}"; - var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; - var noteableType = "#{votable.class.name.underscore}"; - var noteableId = "#{votable.id}"; - var unicodes = #{AwardEmoji.unicode.to_json}; - - window.awardsHandler = new AwardsHandler( - getEmojisUrl, - postEmojiUrl, - noteableType, - noteableId, - unicodes - ); diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f9e32337983..d947f105516 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,8 +15,7 @@ class RepositoryForkWorker result = gitlab_shell.fork_repository(source_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.update(import_error: "The project could not be forked.") - project.import_fail + project.mark_import_as_failed('The project could not be forked.') return end @@ -24,8 +23,7 @@ class RepositoryForkWorker unless project.valid_repo? logger.error("Project #{project_id} had an invalid repository after fork") - project.update(import_error: "The forked repository is invalid.") - project.import_fail + project.mark_import_as_failed('The forked repository is invalid.') return end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index fbc7ed63c6a..7d819fe78f8 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,8 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: Gitlab::UrlSanitizer.sanitize(result[:message])) - project.import_fail + project.mark_import_as_failed(result[:message]) return end diff --git a/config/application.rb b/config/application.rb index de2bb08c978..49d4d3ba555 100644 --- a/config/application.rb +++ b/config/application.rb @@ -81,6 +81,8 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "graphs/application.js" + config.assets.precompile << "users/application.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb deleted file mode 100644 index 05a1852cdbd..00000000000 --- a/config/initializers/devise_async.rb +++ /dev/null @@ -1 +0,0 @@ -Devise::Async.backend = :sidekiq diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 9e8b0131f8f..3d1a41a4652 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -8,3 +8,7 @@ # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end +# +ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable %w(award_emoji) +end diff --git a/config/routes.rb b/config/routes.rb index 428302d0fd7..27ab79d68f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -343,8 +343,9 @@ Rails.application.routes.draw do resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] - resource :two_factor_auth, only: [:new, :create, :destroy] do + resource :two_factor_auth, only: [:show, :create, :destroy] do member do + post :create_u2f post :codes patch :skip end @@ -652,6 +653,7 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription + post :toggle_award_emoji post :remove_wip end @@ -727,6 +729,7 @@ Rails.application.routes.draw do resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + post :toggle_award_emoji get :referenced_merge_requests get :related_branches get :can_create_branch @@ -757,10 +760,6 @@ Rails.application.routes.draw do member do delete :delete_attachment end - - collection do - post :award_toggle - end end resources :uploads, only: [:create] do diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb new file mode 100644 index 00000000000..2ead181921b --- /dev/null +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -0,0 +1,14 @@ +class AddAwardEmoji < ActiveRecord::Migration + def change + create_table :award_emoji do |t| + t.string :name + t.references :user + t.references :awardable, polymorphic: true + + t.timestamps + end + + add_index :award_emoji, :user_id + add_index :award_emoji, [:awardable_type, :awardable_id] + end +end diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb new file mode 100644 index 00000000000..073bbc0fc2a --- /dev/null +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -0,0 +1,9 @@ +class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration + def change + def up + execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)" + + execute "DELETE FROM notes WHERE is_award = true" + end + end +end diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb new file mode 100644 index 00000000000..da16372a297 --- /dev/null +++ b/db/migrate/20160416190505_remove_note_is_award.rb @@ -0,0 +1,5 @@ +class RemoveNoteIsAward < ActiveRecord::Migration + def change + remove_column :notes, :is_award, :boolean + end +end diff --git a/db/migrate/20160425045124_create_u2f_registrations.rb b/db/migrate/20160425045124_create_u2f_registrations.rb new file mode 100644 index 00000000000..93bdd9de2eb --- /dev/null +++ b/db/migrate/20160425045124_create_u2f_registrations.rb @@ -0,0 +1,13 @@ +class CreateU2fRegistrations < ActiveRecord::Migration + def change + create_table :u2f_registrations do |t| + t.text :certificate + t.string :key_handle, index: true + t.string :public_key + t.integer :counter + t.references :user, index: true, foreign_key: true + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b2af810f600..9b991f347a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,6 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20160530150109) do - # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "pg_trgm" @@ -100,6 +99,18 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree + create_table "award_emoji", force: :cascade do |t| + t.string "name" + t.integer "user_id" + t.integer "awardable_id" + t.string "awardable_type" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree + add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree + create_table "broadcast_messages", force: :cascade do |t| t.text "message", null: false t.datetime "starts_at" @@ -638,7 +649,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false t.string "type" end @@ -646,7 +656,6 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree - add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -930,6 +939,19 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree + create_table "u2f_registrations", force: :cascade do |t| + t.text "certificate" + t.string "key_handle" + t.string "public_key" + t.integer "counter" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree + add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -1037,4 +1059,5 @@ ActiveRecord::Schema.define(version: 20160530150109) do add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree + add_foreign_key "u2f_registrations", "users" end diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 49ff5d536a1..537f4f3501d 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -2,8 +2,8 @@ ## Required NFS Server features -**File locking**: GitLab **requires** file locking which is only supported -natively in NFS version 4. NFSv3 also supports locking as long as +**File locking**: GitLab **requires** advisory file locking, which is only +supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index 134a7583762..a776cd3f05e 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -150,6 +150,14 @@ To output a backtrace from all threads at once: apply all thread bt ``` +Once you're done debugging with `gdb`, be sure to detach from the process and +exit: + +``` +detach +exit +``` + ## Check for blocking queries Sometimes the speed at which Sidekiq processes jobs can be so fast that it can diff --git a/doc/api/builds.md b/doc/api/builds.md index 4c0a47d1ea0..5669bd0cdda 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -278,6 +278,30 @@ Response: [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 +## Get a trace file + +Get a trace of a specific build of a project + +``` +GET /projects/:id/builds/:build_id/trace +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build_id | integer | yes | The ID of a build | + +``` +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +``` + +Response: + +| Status | Description | +|-----------|-----------------------------------| +| 200 | Serves the trace file | +| 404 | Build not found or no trace file | + ## Cancel a build Cancel a single build of a project diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 8217e30fe25..16b892dc3b7 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -413,11 +413,13 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c Merge changes submitted with MR using this API. -If merge success you get `200 OK`. +If the merge succeeds you'll get a `200 OK`. -If it has some conflicts and can not be merged - you get 405 and error message 'Branch cannot be merged' +If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged' -If merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' +If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed' + +If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch' If you don't have permissions to accept this merge request - you'll get a 401 @@ -431,7 +433,8 @@ Parameters: - `merge_request_id` (required) - ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch -- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds +- `merged_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds +- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail ```json { diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index b4dcb748351..23760a14b39 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -33,4 +33,24 @@ be under 'Wiki' tab and so on and so forth. We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide part of the UI for smaller resolutions in favor of a better user experience. However core functionality like browsing files, creating issues, writing comments, should -be available on all resolutions.
\ No newline at end of file +be available on all resolutions. + +## Icons + +* `trash` icon for button or link that does destructive action like removing +information from database or file system +* `x` icon for closing/hiding UI element. For example close modal window +* `pencil` icon for edit button or link +* `eye` icon for subscribe action +* `rss` for rss/atom feed +* `plus` for link or dropdown that lead to page where you create new object (For example new issue page) + + +## Buttons + +* Button should contain icon or text. Exceptions should be approved by UX designer. +* Use gray button on white background or white button on gray background. +* Use red button for destructive actions (not revertable). For example removing issue. +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. + diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 8cbd53cc27a..09c6211b3ab 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -150,3 +150,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) - Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled. +- Edge (Latest stable version) diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png Binary files differnew file mode 100644 index 00000000000..b9138ff60db --- /dev/null +++ b/doc/profile/2fa_u2f_authenticate.png diff --git a/doc/profile/2fa_u2f_register.png b/doc/profile/2fa_u2f_register.png Binary files differnew file mode 100644 index 00000000000..15b3683ef73 --- /dev/null +++ b/doc/profile/2fa_u2f_register.png diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index a0e23c1586c..82505b13401 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -8,12 +8,27 @@ your phone. By enabling 2FA, the only way someone other than you can log into your account is to know your username and password *and* have access to your phone. -#### Note +> **Note:** When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you lose your codes for GitLab.com, we can't disable or recover them. +In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as +the second factor of authentication. Once enabled, in addition to supplying your username and +password to login, you'll be prompted to activate your U2F device (usually by pressing +a button on it), and it will perform secure authentication on your behalf. + +> **Note:** Support for U2F devices was added in version 8.8 + +The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend +that you set up both methods of two-factor authentication, so you can still access your account +from other browsers. + +> **Note:** GitLab officially only supports [Yubikey] U2F devices. + ## Enabling 2FA +### Enable 2FA via mobile application + **In GitLab:** 1. Log in to your GitLab account. @@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them. 1. Click **Submit**. If the pin you entered was correct, you'll see a message indicating that -Two-factor Authentication has been enabled, and you'll be presented with a list +Two-Factor Authentication has been enabled, and you'll be presented with a list of recovery codes. +### Enable 2FA via U2F device + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-Factor Authentication**. +1. Plug in your U2F device. +1. Click on **Setup New U2F Device**. +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device was successfully set up. +Click on **Register U2F Device** to complete the process. + +![Two-Factor U2F Setup](2fa_u2f_register.png) + ## Recovery Codes Should you ever lose access to your phone, you can use one of the ten provided @@ -51,21 +83,39 @@ account. If you lose the recovery codes or just want to generate new ones, you can do so from the **Profile Settings** > **Account** page where you first enabled 2FA. +> **Note:** Recovery codes are not generated for U2F devices. + ## Logging in with 2FA Enabled Logging in with 2FA enabled is only slightly different than a normal login. Enter your username and password credentials as you normally would, and you'll -be presented with a second prompt for an authentication code. Enter the pin from -your phone's application or a recovery code to log in. +be presented with a second prompt, depending on which type of 2FA you've enabled. + +### Log in via mobile application + +Enter the pin from your phone's application or a recovery code to log in. -![Two-factor authentication on sign in](2fa_auth.png) +![Two-Factor Authentication on sign in via OTP](2fa_auth.png) + +### Log in via U2F device + +1. Click **Login via U2F Device** +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device responded to the authentication request. +Click on **Authenticate via U2F Device** to complete the process. + +![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png) ## Disabling 2FA 1. Log in to your GitLab account. 1. Go to your **Profile Settings**. 1. Go to **Account**. -1. Click **Disable Two-factor Authentication**. +1. Click **Disable**, under **Two-Factor Authentication**. + +This will clear all your two-factor authentication registrations, including mobile +applications and U2F devices. ## Note to GitLab administrators @@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://fedorahosted.org/freeotp/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 5125a3e5773..26e67503021 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,14 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Files + Scenario: On Project Code Given I visit my project's files page - Then the active main tab should be Files - And no other main tabs should be active - - Scenario: On Project Commits - Given I visit my project's commits page - Then the active main tab should be Commits + Then the active main tab should be Code And no other main tabs should be active Scenario: On Project Issues @@ -64,40 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Commits + # Sub Tabs: Code + + Scenario: On Project Code/Files + Given I visit my project's files page + Then the active sub tab should be Files + And no other sub tabs should be active + And the active main tab should be Code - Scenario: On Project Commits/Commits + Scenario: On Project Code/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Network + Scenario: On Project Code/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Compare + Scenario: On Project Code/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Branches + Scenario: On Project Code/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Tags + Scenario: On Project Code/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 3c029a973df..550ebccf0d7 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -24,3 +24,4 @@ Feature: Project Builds Summary Then recent build has been erased And recent build summary does not have artifacts widget And recent build summary contains information saying that build has been erased + And the build count cache is updated diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 10e7c234610..c73d0b32337 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,19 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Files + Then the active main tab should be Code + Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Commits + Then the active main tab should be Code + Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Commits + And the active main tab should be Code @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 4a5a71e7e61..745fd3471c4 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -63,10 +63,6 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Tags') end - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index e9e2359146e..374eb0b0e07 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps expect(page).to have_content 'Build has been erased' end end + + step 'the build count cache is updated' do + expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) + end end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index d82c6856918..d34fa694789 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - page.find('.js-label-select').click + page.find('.js-label-select', visible: true).click sleep 0.5 execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 5cd431e05d5..439363e6f14 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do - issue = Issue.find_by(title: 'Release 0.4') - create_list(:upvote_note, 2, project: project, noteable: issue) - create(:downvote_note, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Release 0.4') + create_list(:award_emoji, 2, awardable: awardable) + create(:award_emoji, :downvote, awardable: awardable) end step 'issue "Tweet control" have 1 upvote and 2 downvotes' do - issue = Issue.find_by(title: 'Tweet control') - create(:upvote_note, project: project, noteable: issue) - create_list(:downvote_note, 2, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Tweet control') + create(:award_emoji, :upvote, awardable: awardable) + create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown') end step 'The list should be sorted by "Least popular"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index b30346790eb..1dd6cbef615 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do merge_request = MergeRequest.find_by(title: 'Bug NS-04') - create_list(:upvote_note, 2, project: project, noteable: merge_request) - create(:downvote_note, project: project, noteable: merge_request) + create_list(:award_emoji, 2, awardable: merge_request) + create(:award_emoji, :downvote, awardable: merge_request) end step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do - merge_request = MergeRequest.find_by(title: 'Bug NS-06') - create(:upvote_note, project: project, noteable: merge_request) - create_list(:downvote_note, 2, project: project, noteable: merge_request) + awardable = MergeRequest.find_by(title: 'Bug NS-06') + create(:award_emoji, awardable: awardable) + create_list(:award_emoji, 2, :downvote, awardable: awardable) end step 'The list should be sorted by "Least popular"' do diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 8c1d09d6cc6..47de4b91df1 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index b209020c5a9..bfee8793301 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,12 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Files' do - ensure_active_main_tab('Files') - end - - step 'the active main tab should be Commits' do - ensure_active_main_tab('Commits') + step 'the active main tab should be Code' do + ensure_active_main_tab('Code') end step 'the active main tab should be Graphs' do @@ -51,4 +47,12 @@ module SharedProjectTab step 'the active sub tab should be Network' do ensure_active_sub_tab('Network') end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 790a1869f73..66c138eb902 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -30,7 +30,7 @@ module API expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project - expose :two_factor_enabled + expose :two_factor_enabled?, as: :two_factor_enabled expose :external end @@ -171,15 +171,17 @@ 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 expose :user_notes_count + expose :upvotes, :downvotes end class MergeRequest < ProjectEntity expose :target_branch, :source_branch - expose :upvotes, :downvotes + expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels @@ -217,8 +219,8 @@ module API expose :system?, as: :system expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false - expose :upvote?, as: :upvote - expose :downvote?, as: :downvote + expose(:upvote?) { |note| false } + expose(:downvote?) { |note| false } end class MRNote < Grape::Entity diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4e7de8867b4..db304abe1c3 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -218,6 +218,7 @@ module API # merge_commit_message (optional) - Custom merge commit message # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # sha (optional) - When present, must have the HEAD SHA of the source branch # Example: # PUT /projects/:id/merge_requests/:merge_request_id/merge # @@ -233,6 +234,10 @@ module API render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + if params[:sha] && merge_request.source_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) + end + merge_params = { commit_message: params[:merge_commit_message], should_remove_source_branch: params[:should_remove_source_branch] diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 62161aadb9a..9cb14e95ebc 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -57,7 +57,7 @@ module API not_found! "File" unless blob content_type 'text/plain' - header *Gitlab::Workhorse.send_git_blob(repo, blob) + header(*Gitlab::Workhorse.send_git_blob(repo, blob)) end # Get a raw blob contents by blob sha @@ -83,7 +83,7 @@ module API env['api.format'] = :txt content_type blob.mime_type - header *Gitlab::Workhorse.send_git_blob(repo, blob) + header(*Gitlab::Workhorse.send_git_blob(repo, blob)) end # Get a an archive of the repository @@ -98,7 +98,7 @@ module API authorize! :download_code, user_project begin - header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) + header(*Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format])) rescue not_found!('File') end diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb deleted file mode 100644 index b1aecc2e671..00000000000 --- a/lib/award_emoji.rb +++ /dev/null @@ -1,84 +0,0 @@ -class AwardEmoji - CATEGORIES = { - other: "Other", - objects: "Objects", - places: "Places", - travel_places: "Travel", - emoticons: "Emoticons", - objects_symbols: "Symbols", - nature: "Nature", - celebration: "Celebration", - people: "People", - activity: "Activity", - flags: "Flags", - food_drink: "Food" - }.with_indifferent_access - - CATEGORY_ALIASES = { - symbols: "objects_symbols", - foods: "food_drink", - travel: "travel_places" - }.with_indifferent_access - - def self.normilize_emoji_name(name) - aliases[name] || name - end - - def self.emoji_by_category - unless @emoji_by_category - @emoji_by_category = Hash.new { |h, key| h[key] = [] } - - emojis.each do |emoji_name, data| - data["name"] = emoji_name - - # Skip Fitzpatrick(tone) modifiers - next if data["category"] == "modifier" - - category = CATEGORY_ALIASES[data["category"]] || data["category"] - - @emoji_by_category[category] << data - end - - @emoji_by_category = @emoji_by_category.sort.to_h - end - - @emoji_by_category - end - - def self.emojis - @emojis ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) - JSON.parse(File.read(json_path)) - end - end - - def self.unicode - @unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!) - end - - def self.aliases - @aliases ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) - JSON.parse(File.read(json_path)) - end - end - - # Returns an Array of Emoji names and their asset URLs. - def self.urls - @urls ||= begin - path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') - prefix = Gitlab::Application.config.assets.prefix - digest = Gitlab::Application.config.assets.digest - - JSON.parse(File.read(path)).map do |hash| - if digest - fname = "#{hash['unicode']}-#{hash['digest']}" - else - fname = hash['unicode'] - end - - { name: hash['name'], path: "#{prefix}/#{fname}.png" } - end - end - end -end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 67b2a64bd10..22319ec6623 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -86,9 +86,9 @@ module Backup def report_success(success) if success - $progress.puts '[DONE]'.green + $progress.puts '[DONE]'.color(:green) else - $progress.puts '[FAILED]'.red + $progress.puts '[FAILED]'.color(:red) end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 660ca8c2923..9dd665441a0 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -27,9 +27,9 @@ module Backup # Set file permissions on open to prevent chmod races. tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "creating archive #{tar_file} failed".red + puts "creating archive #{tar_file} failed".color(:red) abort 'Backup failed' end @@ -43,7 +43,7 @@ module Backup connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - $progress.puts "skipped".yellow + $progress.puts "skipped".color(:yellow) return end @@ -53,9 +53,9 @@ module Backup if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "uploading backup to #{remote_directory} failed".red + puts "uploading backup to #{remote_directory} failed".color(:red) abort 'Backup failed' end end @@ -67,9 +67,9 @@ module Backup next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "deleting tmp directory '#{dir}' failed".red + puts "deleting tmp directory '#{dir}' failed".color(:red) abort 'Backup failed' end end @@ -95,9 +95,9 @@ module Backup end end - $progress.puts "done. (#{removed} removed)".green + $progress.puts "done. (#{removed} removed)".color(:green) else - $progress.puts "skipping".yellow + $progress.puts "skipping".color(:yellow) end end @@ -124,20 +124,20 @@ module Backup $progress.print "Unpacking backup ... " unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".red + puts "unpacking backup failed".color(:red) exit 1 else - $progress.puts "done".green + $progress.puts "done".color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - puts "GitLab version mismatch:".red - puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red - puts " Please switch to the following version and try again:".red - puts " version: #{settings[:gitlab_version]}".red + puts "GitLab version mismatch:".color(:red) + puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) + puts " Please switch to the following version and try again:".color(:red) + puts " version: #{settings[:gitlab_version]}".color(:red) puts puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index a82a7e1f7bf..7b91215d50b 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -14,14 +14,14 @@ module Backup FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace if project.empty_repo? - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" puts output abort 'Backup failed' @@ -33,14 +33,14 @@ module Backup if File.exists?(path_to_repo(wiki)) $progress.print " * #{wiki.path_with_namespace} ... " if wiki.repository.empty? - $progress.puts " [SKIPPED]".cyan + $progress.puts " [SKIPPED]".color(:cyan) else cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Backup failed' end @@ -71,9 +71,9 @@ module Backup end if system(*cmd, silent) - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end @@ -90,21 +90,21 @@ module Backup cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) if system(*cmd, silent) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end end end - $progress.print 'Put GitLab hooks in repositories dirs'.yellow + $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" if system(cmd) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd}" end diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb index 9e75edd4d4c..beb21b19ab3 100644 --- a/lib/banzai/filter/inline_diff_filter.rb +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -8,15 +8,19 @@ module Banzai next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) content = node.to_html - content = content.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>') - content = content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>') + html_content = inline_diff_filter(content) - next if html == content + next if content == html_content - node.replace(content) + node.replace(html_content) end doc end + + def inline_diff_filter(text) + html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>') + html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>') + end end end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 41ae0e1f9cc..2d6f34c9cd8 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -68,6 +68,8 @@ module Banzai # by `ignore_ancestor_query`. Link tags are not processed if they have a # "gfm" class or the "href" attribute is empty. def each_node + return to_enum(__method__) unless block_given? + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] | descendant-or-self::a[ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") @@ -78,6 +80,11 @@ module Banzai end end + # Returns an Array containing all HTML nodes. + def nodes + @nodes ||= each_node.to_a + end + # Yields the link's URL and text whenever the node is a valid <a> tag. def yield_valid_link(node) link = CGI.unescape(node.attr('href').to_s) diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 331d8007257..5b0a6d8541b 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -29,7 +29,7 @@ module Banzai ref_pattern = User.reference_pattern ref_pattern_start = /\A#{ref_pattern}\z/ - each_node do |node| + nodes.each do |node| if text_node?(node) replace_text_when_pattern_matches(node, ref_pattern) do |content| user_link_filter(content) @@ -59,7 +59,7 @@ module Banzai self.class.references_in(text) do |match, username| if username == 'all' link_to_all(link_text: link_text) - elsif namespace = Namespace.find_by(path: username) + elsif namespace = namespaces[username] link_to_namespace(namespace, link_text: link_text) || match else match @@ -67,6 +67,31 @@ module Banzai end end + # Returns a Hash containing all Namespace objects for the username + # references in the current document. + # + # The keys of this Hash are the namespace paths, the values the + # corresponding Namespace objects. + def namespaces + @namespaces ||= + Namespace.where(path: usernames).each_with_object({}) do |row, hash| + hash[row.path] = row + end + end + + # Returns all usernames referenced in the current document. + def usernames + refs = Set.new + + nodes.each do |node| + node.to_html.scan(User.reference_pattern) do + refs << $~[:user] + end + end + + refs.to_a + end + private def urls diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb new file mode 100644 index 00000000000..51b1df9ecbd --- /dev/null +++ b/lib/gitlab/award_emoji.rb @@ -0,0 +1,84 @@ +module Gitlab + class AwardEmoji + CATEGORIES = { + other: "Other", + objects: "Objects", + places: "Places", + travel_places: "Travel", + emoticons: "Emoticons", + objects_symbols: "Symbols", + nature: "Nature", + celebration: "Celebration", + people: "People", + activity: "Activity", + flags: "Flags", + food_drink: "Food" + }.with_indifferent_access + + CATEGORY_ALIASES = { + symbols: "objects_symbols", + foods: "food_drink", + travel: "travel_places" + }.with_indifferent_access + + def self.normalize_emoji_name(name) + aliases[name] || name + end + + def self.emoji_by_category + unless @emoji_by_category + @emoji_by_category = Hash.new { |h, key| h[key] = [] } + + emojis.each do |emoji_name, data| + data["name"] = emoji_name + + # Skip Fitzpatrick(tone) modifiers + next if data["category"] == "modifier" + + category = CATEGORY_ALIASES[data["category"]] || data["category"] + + @emoji_by_category[category] << data + end + + @emoji_by_category = @emoji_by_category.sort.to_h + end + + @emoji_by_category + end + + def self.emojis + @emojis ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) + JSON.parse(File.read(json_path)) + end + end + + def self.aliases + @aliases ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) + JSON.parse(File.read(json_path)) + end + end + + # Returns an Array of Emoji names and their asset URLs. + def self.urls + @urls ||= begin + path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + prefix = Gitlab::Application.config.assets.prefix + digest = Gitlab::Application.config.assets.digest + + JSON.parse(File.read(path)).map do |hash| + if digest + fname = "#{hash['unicode']}-#{hash['digest']}" + else + fname = hash['unicode'] + end + + { name: hash['name'], path: "#{prefix}/#{fname}.png" } + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 408d9b79632..9d077e79c39 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -89,11 +89,11 @@ module Gitlab end end - delete_refs(branches_removed) - true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message + ensure + delete_refs(branches_removed) end def create_refs(branches) diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb index baf52ff750d..8684b4636ea 100644 --- a/lib/gitlab/key_fingerprint.rb +++ b/lib/gitlab/key_fingerprint.rb @@ -17,9 +17,9 @@ module Gitlab file.rewind cmd = [] - cmd.push *%W(ssh-keygen) - cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? - cmd.push *%W(-lf #{file.path}) + cmd.push('ssh-keygen') + cmd.push('-E', 'md5') if explicit_fingerprint_algorithm? + cmd.push('-lf', file.path) cmd_output, cmd_status = popen(cmd, '/tmp') end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index aff7ccb157f..f9bb5775323 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -93,6 +93,7 @@ module Gitlab end protected + def base_config Gitlab.config.ldap end diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 50b0dd32380..5764ab15652 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -39,7 +39,7 @@ module Gitlab request_url = URI.join(base_url, project_path) domain_path = strip_url(request_url.to_s) - "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"; + "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n" end def strip_url(url) diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 2ef0e982256..7cf506ebe64 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -5,7 +5,7 @@ module Gitlab SeedFu.quiet = true yield SeedFu.quiet = false - puts "\nOK".green + puts "\nOK".color(:green) end def self.by_user(user) diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 596eaca6d0d..9ee72fde92f 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -40,14 +40,14 @@ namespace :gitlab do removed. MSG ask_to_continue - puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow + puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) sleep(5) end # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade - $progress.puts 'Cleaning the database ... '.blue + $progress.puts 'Cleaning the database ... '.color(:blue) Rake::Task['gitlab:db:drop_tables'].invoke - $progress.puts 'done'.green + $progress.puts 'done'.color(:green) Rake::Task['gitlab:backup:db:restore'].invoke end Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories') @@ -63,141 +63,141 @@ namespace :gitlab do namespace :repo do task create: :environment do - $progress.puts "Dumping repositories ...".blue + $progress.puts "Dumping repositories ...".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("repositories") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Repository.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring repositories ...".blue + $progress.puts "Restoring repositories ...".color(:blue) Backup::Repository.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :db do task create: :environment do - $progress.puts "Dumping database ... ".blue + $progress.puts "Dumping database ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("db") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Database.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring database ... ".blue + $progress.puts "Restoring database ... ".color(:blue) Backup::Database.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :builds do task create: :environment do - $progress.puts "Dumping builds ... ".blue + $progress.puts "Dumping builds ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("builds") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Builds.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring builds ... ".blue + $progress.puts "Restoring builds ... ".color(:blue) Backup::Builds.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :uploads do task create: :environment do - $progress.puts "Dumping uploads ... ".blue + $progress.puts "Dumping uploads ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("uploads") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Uploads.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring uploads ... ".blue + $progress.puts "Restoring uploads ... ".color(:blue) Backup::Uploads.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :artifacts do task create: :environment do - $progress.puts "Dumping artifacts ... ".blue + $progress.puts "Dumping artifacts ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Artifacts.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring artifacts ... ".blue + $progress.puts "Restoring artifacts ... ".color(:blue) Backup::Artifacts.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :lfs do task create: :environment do - $progress.puts "Dumping lfs objects ... ".blue + $progress.puts "Dumping lfs objects ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("lfs") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Lfs.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring lfs objects ... ".blue + $progress.puts "Restoring lfs objects ... ".color(:blue) Backup::Lfs.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :registry do task create: :environment do - $progress.puts "Dumping container registry images ... ".blue + $progress.puts "Dumping container registry images ... ".color(:blue) if Gitlab.config.registry.enabled if ENV["SKIP"] && ENV["SKIP"].include?("registry") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Registry.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end else - $progress.puts "[DISABLED]".cyan + $progress.puts "[DISABLED]".color(:cyan) end end task restore: :environment do - $progress.puts "Restoring container registry images ... ".blue + $progress.puts "Restoring container registry images ... ".color(:blue) if Gitlab.config.registry.enabled Backup::Registry.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) else - $progress.puts "[DISABLED]".cyan + $progress.puts "[DISABLED]".color(:cyan) end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index fad89c73762..12d6ac45fb6 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -50,14 +50,14 @@ namespace :gitlab do end if correct_options.all? - puts "yes".green + puts "yes".color(:green) else print "Trying to fix Git error automatically. ..." if auto_fix_git_config(options) - puts "Success".green + puts "Success".color(:green) else - puts "Failed".red + puts "Failed".color(:red) try_fixing_it( sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") ) @@ -74,9 +74,9 @@ namespace :gitlab do database_config_file = Rails.root.join("config", "database.yml") if File.exists?(database_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/database.yml.<your db> to config/database.yml", "Check that the information in config/database.yml is correct" @@ -95,9 +95,9 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") if File.exists?(gitlab_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/gitlab.yml.example to config/gitlab.yml", "Update config/gitlab.yml to match your setup" @@ -114,14 +114,14 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") unless File.exists?(gitlab_config_file) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) end # omniauth or ldap could have been deleted from the file unless Gitlab.config['git_host'] - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Backup your config/gitlab.yml", "Copy config/gitlab.yml.example to config/gitlab.yml", @@ -138,16 +138,16 @@ namespace :gitlab do print "Init script exists? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end script_path = "/etc/init.d/gitlab" if File.exists?(script_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Install the init script" ) @@ -162,7 +162,7 @@ namespace :gitlab do print "Init script up-to-date? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end @@ -170,7 +170,7 @@ namespace :gitlab do script_path = "/etc/init.d/gitlab" unless File.exists?(script_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end @@ -178,9 +178,9 @@ namespace :gitlab do script_content = File.read(script_path) if recipe_content == script_content - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Redownload the init script" ) @@ -197,9 +197,9 @@ namespace :gitlab do migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status)) unless migration_status =~ /down\s+\d{14}/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production") ) @@ -210,13 +210,13 @@ namespace :gitlab do def check_orphaned_group_members print "Database contains orphaned GroupMembers? ... " if GroupMember.where("user_id not in (select id from users)").count > 0 - puts "yes".red + puts "yes".color(:red) try_fixing_it( "You can delete the orphaned records using something along the lines of:", sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'") ) else - puts "no".green + puts "no".color(:green) end end @@ -226,9 +226,9 @@ namespace :gitlab do log_path = Rails.root.join("log") if File.writable?(log_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{log_path}", "sudo chmod -R u+rwX #{log_path}" @@ -246,9 +246,9 @@ namespace :gitlab do tmp_path = Rails.root.join("tmp") if File.writable?(tmp_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{tmp_path}", "sudo chmod -R u+rwX #{tmp_path}" @@ -264,7 +264,7 @@ namespace :gitlab do print "Uploads directory setup correctly? ... " unless File.directory?(Rails.root.join('public/uploads')) - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" ) @@ -280,16 +280,16 @@ namespace :gitlab do if File.stat(upload_path).mode == 040700 unless Dir.exists?(upload_path_tmp) - puts 'skipped (no tmp uploads folder yet)'.magenta + puts 'skipped (no tmp uploads folder yet)'.color(:magenta) return end # If tmp upload dir has incorrect permissions, assume others do as well # Verify drwx------ permissions if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R #{gitlab_user} #{upload_path}", "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", @@ -301,7 +301,7 @@ namespace :gitlab do fix_and_rerun end else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chmod 700 #{upload_path}" ) @@ -320,9 +320,9 @@ namespace :gitlab do redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) if redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your redis server to a version >= #{min_redis_version}" ) @@ -361,10 +361,10 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path if File.exists?(repo_base_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts "#{repo_base_path} is missing".red + puts "no".color(:red) + puts "#{repo_base_path} is missing".color(:red) try_fixing_it( "This should have been created when setting up GitLab Shell.", "Make sure it's set correctly in config/gitlab.yml", @@ -382,14 +382,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end unless File.symlink?(repo_base_path) - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Make sure it's set to the real directory in config/gitlab.yml" ) @@ -402,14 +402,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}", @@ -429,17 +429,17 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end uid = uid_for(gitlab_shell_ssh_user) gid = gid_for(gitlab_shell_owner_group) if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue + puts "no".color(:red) + puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue) try_fixing_it( "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" ) @@ -456,7 +456,7 @@ namespace :gitlab do gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -466,12 +466,12 @@ namespace :gitlab do project_hook_directory = File.join(project.repository.path_to_repo, "hooks") if project.empty_repo? - puts "repository is empty".magenta + puts "repository is empty".color(:magenta) elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) - puts 'ok'.green + puts 'ok'.color(:green) else - puts "wrong or missing hooks".red + puts "wrong or missing hooks".color(:red) try_fixing_it( sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"), 'Check the hooks_path in config/gitlab.yml', @@ -491,9 +491,9 @@ namespace :gitlab do check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) puts "Running #{check_cmd}" if system(check_cmd, chdir: gitlab_shell_repo_base) - puts 'gitlab-shell self-check successful'.green + puts 'gitlab-shell self-check successful'.color(:green) else - puts 'gitlab-shell self-check failed'.red + puts 'gitlab-shell self-check failed'.color(:red) try_fixing_it( 'Make sure GitLab is running;', 'Check the gitlab-shell configuration file:', @@ -507,7 +507,7 @@ namespace :gitlab do print "projects have namespace: ... " unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -516,9 +516,9 @@ namespace :gitlab do print sanitized_message(project) if project.namespace - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Migrate global projects" ) @@ -576,9 +576,9 @@ namespace :gitlab do print "Running? ... " if sidekiq_process_count > 0 - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/background_jobs start") ) @@ -596,9 +596,9 @@ namespace :gitlab do print 'Number of Sidekiq processes ... ' if process_count == 1 - puts '1'.green + puts '1'.color(:green) else - puts "#{process_count}".red + puts "#{process_count}".color(:red) try_fixing_it( 'sudo service gitlab stop', "sudo pkill -u #{gitlab_user} -f sidekiq", @@ -646,16 +646,16 @@ namespace :gitlab do print "Init.d configured correctly? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end path = "/etc/default/gitlab" if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in the init.d configuration." ) @@ -672,9 +672,9 @@ namespace :gitlab do path = Rails.root.join("Procfile") if File.exist?(path) && File.read(path) =~ /^mail_room:/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in your Procfile." ) @@ -691,14 +691,14 @@ namespace :gitlab do path = "/etc/default/gitlab" unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if mail_room_running? - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/mail_room start") ) @@ -729,9 +729,9 @@ namespace :gitlab do end if connected - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Check that the information in config/gitlab.yml is correct" ) @@ -799,7 +799,7 @@ namespace :gitlab do namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| - username = args[:username] || prompt("Check repository integrity for which username? ".blue) + username = args[:username] || prompt("Check repository integrity for which username? ".color(:blue)) user = User.find_by(username: username) if user repo_dirs = user.authorized_projects.map do |p| @@ -811,7 +811,7 @@ namespace :gitlab do repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } else - puts "\nUser '#{username}' not found".red + puts "\nUser '#{username}' not found".color(:red) end end end @@ -820,13 +820,13 @@ namespace :gitlab do ########################## def fix_and_rerun - puts " Please #{"fix the error above"} and rerun the checks.".red + puts " Please #{"fix the error above"} and rerun the checks.".color(:red) end def for_more_information(*sources) sources = sources.shift if sources.first.is_a?(Array) - puts " For more information see:".blue + puts " For more information see:".color(:blue) sources.each do |source| puts " #{source}" end @@ -834,7 +834,7 @@ namespace :gitlab do def finished_checking(component) puts "" - puts "Checking #{component.yellow} ... #{"Finished".green}" + puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}" puts "" end @@ -855,14 +855,14 @@ namespace :gitlab do end def start_checking(component) - puts "Checking #{component.yellow} ..." + puts "Checking #{component.color(:yellow)} ..." puts "" end def try_fixing_it(*steps) steps = steps.shift if steps.first.is_a?(Array) - puts " Try fixing it:".blue + puts " Try fixing it:".color(:blue) steps.each do |step| puts " #{step}" end @@ -874,9 +874,9 @@ namespace :gitlab do print "GitLab Shell version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "OK (#{current_version})".green + puts "OK (#{current_version})".color(:green) else - puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red + puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) end end @@ -887,9 +887,9 @@ namespace :gitlab do print "Ruby version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your ruby to a version >= #{required_version} from #{current_version}" ) @@ -905,9 +905,9 @@ namespace :gitlab do print "Git version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your git to a version >= #{required_version} from #{current_version}" ) @@ -925,9 +925,9 @@ namespace :gitlab do def sanitized_message(project) if should_sanitize? - "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " + "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... " else - "#{project.name_with_namespace.yellow} ... " + "#{project.name_with_namespace.color(:yellow)} ... " end end @@ -940,7 +940,7 @@ namespace :gitlab do end def check_repo_integrity(repo_dir) - puts "\nChecking repo at #{repo_dir.yellow}" + puts "\nChecking repo at #{repo_dir.color(:yellow)}" git_fsck(repo_dir) check_config_lock(repo_dir) @@ -948,25 +948,25 @@ namespace :gitlab do end def git_fsck(repo_dir) - puts "Running `git fsck`".yellow + puts "Running `git fsck`".color(:yellow) system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) end def check_config_lock(repo_dir) config_exists = File.exist?(File.join(repo_dir,'config.lock')) - config_output = config_exists ? 'yes'.red : 'no'.green - puts "'config.lock' file exists?".yellow + " ... #{config_output}" + config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) + puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" end def check_ref_locks(repo_dir) lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock')) if lock_files.present? - puts "Ref lock files exist:".red + puts "Ref lock files exist:".color(:red) lock_files.each do |lock_file| puts " #{lock_file}" end else - puts "No ref lock files exist".green + puts "No ref lock files exist".color(:green) end end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 9f5852ac613..ab0028d6603 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -10,7 +10,7 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path all_dirs = Dir.glob(git_base_path + '/*') - puts git_base_path.yellow + puts git_base_path.color(:yellow) puts "Looking for directories to remove... " all_dirs.reject! do |dir| @@ -29,17 +29,17 @@ namespace :gitlab do if remove_flag if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".red + puts "Removed...#{dir_path}".color(:red) else - puts "Cannot remove #{dir_path}".red + puts "Cannot remove #{dir_path}".color(:red) end else - puts "Can be removed: #{dir_path}".red + puts "Can be removed: #{dir_path}".color(:red) end end unless remove_flag - puts "To cleanup this directories run this command with REMOVE=true".yellow + puts "To cleanup this directories run this command with REMOVE=true".color(:yellow) end end @@ -75,19 +75,19 @@ namespace :gitlab do next unless user.ldap_user? print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." if Gitlab::LDAP::Access.allowed?(user) - puts " [OK]".green + puts " [OK]".color(:green) else if block_flag user.block! unless user.blocked? - puts " [BLOCKED]".red + puts " [BLOCKED]".color(:red) else - puts " [NOT IN LDAP]".yellow + puts " [NOT IN LDAP]".color(:yellow) end end end unless block_flag - puts "To block these users run this command with BLOCK=true".yellow + puts "To block these users run this command with BLOCK=true".color(:yellow) end end end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 86f5d65f128..86584e91093 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -3,22 +3,22 @@ namespace :gitlab do desc 'GitLab | Manually insert schema migration version' task :mark_migration_complete, [:version] => :environment do |_, args| unless args[:version] - puts "Must specify a migration version as an argument".red + puts "Must specify a migration version as an argument".color(:red) exit 1 end version = args[:version].to_i if version == 0 - puts "Version '#{args[:version]}' must be a non-zero integer".red + puts "Version '#{args[:version]}' must be a non-zero integer".color(:red) exit 1 end sql = "INSERT INTO schema_migrations (version) VALUES (#{version})" begin ActiveRecord::Base.connection.execute(sql) - puts "Successfully marked '#{version}' as complete".green + puts "Successfully marked '#{version}' as complete".color(:green) rescue ActiveRecord::RecordNotUnique - puts "Migration version '#{version}' is already marked complete".yellow + puts "Migration version '#{version}' is already marked complete".color(:yellow) end end diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 65ee430d550..f9834a4dae8 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -5,7 +5,7 @@ namespace :gitlab do task repack: :environment do failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -15,7 +15,7 @@ namespace :gitlab do task gc: :environment do failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -25,7 +25,7 @@ namespace :gitlab do task prune: :environment do failures = perform_git_cmd(%W(git prune), "Git Prune") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -47,7 +47,7 @@ namespace :gitlab do end def output_failures(failures) - puts "The following repositories reported errors:".red + puts "The following repositories reported errors:".color(:red) failures.each { |f| puts "- #{f}" } end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 1c04f47f08f..4753f00c26a 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -23,7 +23,7 @@ namespace :gitlab do group_name, name = File.split(path) group_name = nil if group_name == '.' - puts "Processing #{repo_path}".yellow + puts "Processing #{repo_path}".color(:yellow) if path.end_with?('.wiki') puts " * Skipping wiki repo" @@ -51,9 +51,9 @@ namespace :gitlab do group.path = group_name group.owner = user if group.save - puts " * Created Group #{group.name} (#{group.id})".green + puts " * Created Group #{group.name} (#{group.id})".color(:green) else - puts " * Failed trying to create group #{group.name}".red + puts " * Failed trying to create group #{group.name}".color(:red) end end # set project group @@ -63,17 +63,17 @@ namespace :gitlab do project = Projects::CreateService.new(user, project_params).execute if project.persisted? - puts " * Created #{project.name} (#{repo_path})".green + puts " * Created #{project.name} (#{repo_path})".color(:green) project.update_repository_size project.update_commit_count else - puts " * Failed trying to create #{project.name} (#{repo_path})".red - puts " Errors: #{project.errors.messages}".red + puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) + puts " Errors: #{project.errors.messages}".color(:red) end end end - puts "Done!".green + puts "Done!".color(:green) end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index d6883a563ee..352b566df24 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -15,15 +15,15 @@ namespace :gitlab do rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s) puts "" - puts "System information".yellow - puts "System:\t\t#{os_name || "unknown".red}" + puts "System information".color(:yellow) + puts "System:\t\t#{os_name || "unknown".color(:red)}" puts "Current User:\t#{run(%W(whoami))}" - puts "Using RVM:\t#{rvm_version.present? ? "yes".green : "no"}" + puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}" puts "RVM Version:\t#{rvm_version}" if rvm_version.present? - puts "Ruby Version:\t#{ruby_version || "unknown".red}" - puts "Gem Version:\t#{gem_version || "unknown".red}" - puts "Bundler Version:#{bunder_version || "unknown".red}" - puts "Rake Version:\t#{rake_version || "unknown".red}" + puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}" + puts "Gem Version:\t#{gem_version || "unknown".color(:red)}" + puts "Bundler Version:#{bunder_version || "unknown".color(:red)}" + puts "Rake Version:\t#{rake_version || "unknown".color(:red)}" puts "Sidekiq Version:#{Sidekiq::VERSION}" @@ -39,7 +39,7 @@ namespace :gitlab do omniauth_providers.map! { |provider| provider['name'] } puts "" - puts "GitLab information".yellow + puts "GitLab information".color(:yellow) puts "Version:\t#{Gitlab::VERSION}" puts "Revision:\t#{Gitlab::REVISION}" puts "Directory:\t#{Rails.root}" @@ -47,9 +47,9 @@ namespace :gitlab do puts "URL:\t\t#{Gitlab.config.gitlab.url}" puts "HTTP Clone URL:\t#{http_clone_url}" puts "SSH Clone URL:\t#{ssh_clone_url}" - puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}" - puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}" - puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled + puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}" + puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}" + puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled @@ -60,8 +60,8 @@ namespace :gitlab do end puts "" - puts "GitLab Shell".yellow - puts "Version:\t#{gitlab_shell_version || "unknown".red}" + puts "GitLab Shell".color(:yellow) + puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}" puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}" puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}" diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index dd61632e557..b1648a4602a 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -118,12 +118,12 @@ namespace :gitlab do puts "" unless $?.success? - puts "Failed to add keys...".red + puts "Failed to add keys...".color(:red) exit 1 end rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) exit 1 end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index d33b5b31e18..d0c019044b7 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -2,7 +2,7 @@ module Gitlab class TaskAbortedByUserError < StandardError; end end -String.disable_colorization = true unless STDOUT.isatty +require 'rainbow/ext/string' # Prevent StateMachine warnings from outputting during a cron task StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] @@ -14,7 +14,7 @@ namespace :gitlab do # Returns "yes" the user chose to continue # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue def ask_to_continue - answer = prompt("Do you want to continue (yes/no)? ".blue, %w{yes no}) + answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) raise Gitlab::TaskAbortedByUserError unless answer == "yes" end @@ -98,10 +98,10 @@ namespace :gitlab do gitlab_user = Gitlab.config.gitlab.user current_user = run(%W(whoami)).chomp unless current_user == gitlab_user - puts " Warning ".colorize(:black).on_yellow - puts " You are running as user #{current_user.magenta}, we hope you know what you are doing." + puts " Warning ".color(:black).background(:yellow) + puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." puts " Things may work\/fail for the wrong reasons." - puts " For correct results you should run this as user #{gitlab_user.magenta}." + puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." puts "" end @warned_user_not_gitlab = true diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index 9196677a017..fc0ccc726ed 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -6,17 +6,17 @@ namespace :gitlab do count = scope.count if count > 0 - puts "This will disable 2FA for #{count.to_s.red} users..." + puts "This will disable 2FA for #{count.to_s.color(:red)} users..." begin ask_to_continue scope.find_each(&:disable_two_factor!) - puts "Successfully disabled 2FA for #{count} users.".green + puts "Successfully disabled 2FA for #{count} users.".color(:green) rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) end else - puts "There are currently no users with 2FA enabled.".yellow + puts "There are currently no users with 2FA enabled.".color(:yellow) end end end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake index 9b636f12d9f..3bd10b0208b 100644 --- a/lib/tasks/gitlab/update_commit_count.rake +++ b/lib/tasks/gitlab/update_commit_count.rake @@ -6,15 +6,15 @@ namespace :gitlab do ask_to_continue unless ENV['force'] == 'yes' projects.find_each(batch_size: 100) do |project| - print "#{project.name_with_namespace.yellow} ... " + print "#{project.name_with_namespace.color(:yellow)} ... " unless project.repo_exists? - puts "skipping, because the repo is empty".magenta + puts "skipping, because the repo is empty".color(:magenta) next end project.update_commit_count - puts project.commit_count.to_s.green + puts project.commit_count.to_s.color(:green) end end end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake index 84aa312002b..4fd48cccb1d 100644 --- a/lib/tasks/gitlab/update_gitignore.rake +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -2,14 +2,14 @@ namespace :gitlab do desc "GitLab | Update gitignore" task :update_gitignore do unless clone_gitignores - puts "Cloning the gitignores failed".red + puts "Cloning the gitignores failed".color(:red) return end remove_unneeded_files(gitignore_directory) remove_unneeded_files(global_directory) - puts "Done".green + puts "Done".color(:green) end def clone_gitignores diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index cc0f668474e..f467cc0ee29 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -12,9 +12,9 @@ namespace :gitlab do print "- #{project.name} ... " web_hook = project.hooks.new(url: web_hook_url) if web_hook.save - puts "added".green + puts "added".color(:green) else - print "failed".red + print "failed".color(:red) puts " [#{web_hook.errors.full_messages.to_sentence}]" end end @@ -57,7 +57,7 @@ namespace :gitlab do if namespace Project.in_namespace(namespace.id) else - puts "Namespace not found: #{namespace_path}".red + puts "Namespace not found: #{namespace_path}".color(:red) exit 2 end end diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake index d258c6fd08d..4f2486157b7 100644 --- a/lib/tasks/migrate/migrate_iids.rake +++ b/lib/tasks/migrate/migrate_iids.rake @@ -1,6 +1,6 @@ desc "GitLab | Build internal ids for issues and merge requests" task migrate_iids: :environment do - puts 'Issues'.yellow + puts 'Issues'.color(:yellow) Issue.where(iid: nil).find_each(batch_size: 100) do |issue| begin issue.set_iid @@ -15,7 +15,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Merge Requests'.yellow + puts 'Merge Requests'.color(:yellow) MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| begin mr.set_iid @@ -30,7 +30,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Milestones'.yellow + puts 'Milestones'.color(:yellow) Milestone.where(iid: nil).find_each(batch_size: 100) do |m| begin m.set_iid diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 01d23b89bb7..da255f5464b 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -52,7 +52,7 @@ def run_spinach_tests(tags) tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) puts '' - puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) puts '' sleep(3) success = run_spinach_command(tests) diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 465531b2b36..cd98fecd0c7 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -31,9 +31,9 @@ describe GroupsController do let(:issue_2) { create(:issue, project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: issue_2) - create_list(:upvote_note, 2, project: project, noteable: issue_1) - create_list(:downvote_note, 2, project: project, noteable: issue_2) + create_list(:award_emoji, 3, awardable: issue_2) + create_list(:award_emoji, 2, awardable: issue_1) + create_list(:award_emoji, 2, :downvote, awardable: issue_2,) sign_in(user) end @@ -56,9 +56,9 @@ describe GroupsController do let(:merge_request_2) { create(:merge_request, :simple, source_project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: merge_request_2) - create_list(:upvote_note, 2, project: project, noteable: merge_request_1) - create_list(:downvote_note, 2, project: project, noteable: merge_request_2) + create_list(:award_emoji, 3, awardable: merge_request_2) + create_list(:award_emoji, 2, awardable: merge_request_1) + create_list(:award_emoji, 2, :downvote, awardable: merge_request_2) sign_in(user) end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 4fb1473c2d2..d08d0018b35 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do allow(subject).to receive(:current_user).and_return(user) end - describe 'GET new' do + describe 'GET show' do let(:user) { create(:user) } it 'generates otp_secret for user' do expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once - get :new - get :new # Second hit shouldn't re-generate it + get :show + get :show # Second hit shouldn't re-generate it end it 'assigns qr_code' do code = double('qr code') expect(subject).to receive(:build_qr_code).and_return(code) - get :new + get :show expect(assigns[:qr_code]).to eq code end end @@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true) end - it 'sets two_factor_enabled' do + it 'enables 2fa for the user' do go user.reload @@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do expect(assigns[:qr_code]).to eq code end - it 'renders new' do + it 'renders show' do go - expect(response).to render_template(:new) + expect(response).to render_template(:show) end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8ad73472117..c4b4a888b4e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -122,27 +122,23 @@ describe Projects::BranchesController do let(:branch) { "feature" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with unencoded slashes" do let(:branch) { "improve/awesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with encoded slashes" do let(:branch) { "improve%2Fawesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index c469480b086..78be7e3dc35 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -250,4 +250,20 @@ describe Projects::IssuesController do end end end + + describe 'POST #toggle_award_emoji' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: issue.iid, name: "thumbsup") + end.to change { issue.award_emoji.count }.by(1) + + expect(response.status).to eq(200) + end + end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4f621a43d7e..8499bf07e9f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -185,6 +185,92 @@ describe Projects::MergeRequestsController do end end + describe 'POST #merge' do + let(:base_params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: 'raw' + } + end + + context 'when the user does not have access' do + before do + project.team.truncate + project.team << [user, :reporter] + post :merge, base_params + end + + it 'returns not found' do + expect(response).to be_not_found + end + end + + context 'when the merge request is not mergeable' do + before do + merge_request.update_attributes(title: "WIP: #{merge_request.title}") + + post :merge, base_params + end + + it 'returns :failed' do + expect(assigns(:status)).to eq(:failed) + end + end + + context 'when the sha parameter does not match the source SHA' do + before { post :merge, base_params.merge(sha: 'foo') } + + it 'returns :sha_mismatch' do + expect(assigns(:status)).to eq(:sha_mismatch) + end + end + + context 'when the sha parameter matches the source SHA' do + def merge_with_sha + post :merge, base_params.merge(sha: merge_request.source_sha) + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + + it 'starts the merge immediately' do + expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything) + + merge_with_sha + end + + context 'when merge_when_build_succeeds is passed' do + def merge_when_build_succeeds + post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1') + end + + before do + create(:ci_empty_commit, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + end + + it 'returns :merge_when_build_succeeds' do + merge_when_build_succeeds + + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + end + + it 'sets the MR to merge when the build succeeds' do + service = double(:merge_when_build_succeeds_service) + + expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service) + expect(service).to receive(:execute).with(merge_request) + + merge_when_build_succeeds + end + 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/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index b39d8c8cd5b..4e9bfb0c69b 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -12,7 +12,7 @@ describe SessionsController do post(:create, user: { login: 'invalid', password: 'invalid' }) expect(response) - .to set_flash.now[:alert].to /Invalid login or password/ + .to set_flash.now[:alert].to /Invalid Login or password/ end end @@ -25,10 +25,15 @@ describe SessionsController do expect(response).to set_flash.to /Signed in successfully/ expect(subject.current_user). to eq user end + + it "creates an audit log record" do + expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("standard") + end end end - context 'when using two-factor authentication' do + context 'when using two-factor authentication via OTP' do let(:user) { create(:user, :two_factor) } def authenticate_2fa(user_params) @@ -117,6 +122,25 @@ describe SessionsController do end end end + + it "creates an audit log record" do + expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor") + end + end + + context 'when using two-factor authentication via U2F device' do + let(:user) { create(:user, :two_factor) } + + def authenticate_2fa_u2f(user_params) + post(:create, { user: user_params }, { otp_user_id: user.id }) + end + + it "creates an audit log record" do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device") + end end end end diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb new file mode 100644 index 00000000000..4b858df52c9 --- /dev/null +++ b/spec/factories/award_emoji.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :award_emoji do + name "thumbsup" + user + awardable factory: :issue + + trait :upvote + trait :downvote do + name "thumbsdown" + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index c32e205ee69..696cf276e57 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -16,8 +16,6 @@ FactoryGirl.define do factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] - factory :downvote_note, traits: [:award, :downvote] - factory :upvote_note, traits: [:award, :upvote] trait :on_commit do noteable nil @@ -46,10 +44,6 @@ FactoryGirl.define do system true end - trait :award do - is_award true - end - trait :downvote do note "thumbsdown" end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb new file mode 100644 index 00000000000..df92b079581 --- /dev/null +++ b/spec/factories/u2f_registrations.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :u2f_registration do + certificate { FFaker::BaconIpsum.characters(728) } + key_handle { FFaker::BaconIpsum.characters(86) } + public_key { FFaker::BaconIpsum.characters(88) } + counter 0 + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a9b2148bd2a..c6f7869516e 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -15,14 +15,26 @@ FactoryGirl.define do end trait :two_factor do + two_factor_via_otp + end + + trait :two_factor_via_otp do before(:create) do |user| - user.two_factor_enabled = true + user.otp_required_for_login = true user.otp_secret = User.generate_otp_secret(32) user.otp_grace_period_started_at = Time.now user.generate_otp_backup_codes! end end + trait :two_factor_via_u2f do + transient { registrations_count 5 } + + after(:create) do |user, evaluator| + create_list(:u2f_registration, evaluator.registrations_count, user: user) + end + end + factory :omniauth_user do transient do extern_uid '123456' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 96621843b30..b72ad405479 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication filters' do it 'counts users who have enabled 2FA' do - create(:user, two_factor_enabled: true) + create(:user, :two_factor) visit admin_users_path @@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have enabled 2FA' do - user = create(:user, two_factor_enabled: true) + user = create(:user, :two_factor) visit admin_users_path click_link '2FA Enabled' @@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user, two_factor_enabled: false) + create(:user) visit admin_users_path @@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user, two_factor_enabled: false) + user = create(:user) visit admin_users_path click_link '2FA Disabled' @@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:two_factor_enabled, true) + @user.update_attribute(:otp_required_for_login, true) visit admin_user_path(@user) diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 7a05d30e8b5..e268d76755f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -7,6 +7,7 @@ describe "Builds" do login_as(:user) @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit + @build2 = FactoryGirl.create :ci_build @project = @commit.project @project.team << [@user, :developer] end @@ -66,13 +67,24 @@ describe "Builds" do end describe "GET /:project/builds/:id" do - before do - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } end - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } + context "Build from other project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end context "Download artifacts" do before do @@ -103,51 +115,143 @@ describe "Builds" do end describe "POST /:project/builds/:id/cancel" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link "Cancel" + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } end - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "POST /:project/builds/:id/retry" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" - click_link 'Retry' + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + click_link 'Retry' + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } end - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/download" do - before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.artifacts') { click_link 'Download' } + context "Build from project" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.artifacts') { click_link 'Download' } + end + + it { expect(page.status_code).to eq(200) } + it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } end - it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/raw" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - @build.trace = 'BUILD TRACE' - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + @build.trace = 'BUILD TRACE' + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.build-controls') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end + end + + context "Build from other project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build2.run! + @build2.trace = 'BUILD TRACE' + visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + puts page.status_code + puts current_url + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end + end + end + + describe "GET /:project/builds/:id/trace.json" do + context "Build from project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json) + end + + it { expect(page.status_code).to eq(200) } + end + + context "Build from other project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json) + end + + it { expect(page.status_code).to eq(404) } + end + end + + describe "GET /:project/builds/:id/status" do + context "Build from project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } end - it 'sends the right headers' do - page.within('.build-controls') { click_link 'Raw' } + context "Build from other project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build2) + end - expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + it { expect(page.status_code).to eq(404) } end end end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 41af789aae2..07a854ea014 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsup emoji' do - it 'should increment the thumbsup emoji', js: true do find('[data-emoji="thumbsup"]').click sleep 2 @@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsdown emoji' do - it 'should increment the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click sleep 2 diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb new file mode 100644 index 00000000000..63efecf8780 --- /dev/null +++ b/spec/features/issues/award_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Issue awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add award to issue' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end +end diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb new file mode 100644 index 00000000000..c58b87281a3 --- /dev/null +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -0,0 +1,196 @@ +require 'rails_helper' + +feature 'Issues > Labels bulk assignment', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let!(:project) { create(:project) } + let!(:issue1) { create(:issue, project: project, title: "Issue 1") } + let!(:issue2) { create(:issue, project: project, title: "Issue 2") } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + + context 'as a allowed user', js: true do + before do + project.team << [user, :master] + + login_as user + end + + context 'can bulk assign' do + before do + visit namespace_project_issues_path(project.namespace, project) + end + + context 'a label' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + end + end + end + + context 'multiple labels' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + end + end + + context 'can bulk un-assign' do + context 'all labels to all issues' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check 'check_all_issues' + unmark_labels_in_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + + context 'a label to a issue' do + before do + issue1.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + unmark_labels_in_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'a label and keep the others label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + check_issue issue2 + unmark_labels_in_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + end + end + + context 'as a guest' do + before do + login_as user + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'cannot bulk assign labels' do + it do + expect(page).not_to have_css '.check_all_issues' + expect(page).not_to have_css '.issue-check' + end + end + end + + def open_labels_dropdown(items = [], unmark = false) + page.within('.issues_bulk_update') do + click_button 'Label' + wait_for_ajax + items.map do |item| + click_link item + end + if unmark + items.map do |item| + click_link item + end + end + end + end + + def unmark_labels_in_dropdown(items = []) + open_labels_dropdown(items, true) + end + + def check_issue(issue) + page.within('.issues-list') do + check "selected_issue_#{issue.id}" + end + end + + def update_issues + click_button 'Update issues' + wait_for_ajax + end +end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 466a6f7dfa7..ddbd69b2891 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' feature 'Multiple issue updating from issues#index', feature: true do + include WaitForAjax + let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} @@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'should be set to open' do create_closed - visit namespace_project_issues_path(project.namespace, project) - - find('.issues-state-filters a', text: 'Closed').click + visit namespace_project_issues_path(project.namespace, project, state: 'closed') find('#check_all_issues').click find('.js-issue-status').click @@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button find('.dropdown-menu-user-link', text: user.username).click click_update_issues_button @@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button click_link 'Unassigned' click_update_issues_button - - within first('.issue .controls') do - expect(page).to have_no_selector('.author_link') - end + expect(find('.issue:first-child .controls')).not_to have_css('.author_link') end end @@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - expect(first('.issue')).not_to have_content milestone.title + expect(find('.issue:first-child')).not_to have_content milestone.title end end @@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do create(:issue, project: project, milestone: milestone) end + def click_update_assignee_button + find('.js-update-assignee').click + wait_for_ajax + end + def click_update_issues_button find('.update_selected_issues').click + wait_for_ajax end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9271964166a..460d7f82b36 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -125,7 +125,7 @@ describe 'Issues', feature: true do describe 'Issue info' do it 'excludes award_emoji from comment count' do issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') - create(:upvote_note, noteable: issue, project: project) + create(:award_emoji, awardable: issue) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) @@ -365,13 +365,9 @@ describe 'Issues', feature: true do page.within('.assignee') do expect(page).to have_content "#{@user.name}" - end - find('.block.assignee .edit-link').click - sleep 2 # wait for ajax stuff to complete - first('.dropdown-menu-user-link').click - sleep 2 - page.within('.assignee') do + click_link 'Edit' + click_link 'Unassigned' expect(page).to have_content 'No assignee' end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index a7dc3b2701b..72b5ff231f7 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -33,11 +33,11 @@ feature 'Login', feature: true do before do login_with(user, remember: true) - expect(page).to have_content('Two-factor Authentication') + expect(page).to have_content('Two-Factor Authentication') end def enter_code(code) - fill_in 'Two-factor Authentication code', with: code + fill_in 'Two-Factor Authentication code', with: code click_button 'Verify code' end @@ -127,7 +127,7 @@ feature 'Login', feature: true do user = create(:user, password: 'not-the-default') login_with(user) - expect(page).to have_content('Invalid login or password.') + expect(page).to have_content('Invalid Login or password.') end end @@ -143,12 +143,12 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account before') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account before') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'allows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path click_link 'Configure it later' expect(current_path).to eq root_path @@ -159,26 +159,26 @@ feature 'Login', feature: true do let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'disallows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end end end - context 'without grace pariod defined' do + context 'without grace period defined' do before(:each) do stub_application_setting(two_factor_grace_period: 0) login_with(user) end it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end end end diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb new file mode 100644 index 00000000000..007f67d6080 --- /dev/null +++ b/spec/features/merge_requests/award_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Merge request awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should add award to merge request' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from merge request' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 2835cf44494..737efcef45d 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -4,20 +4,6 @@ describe 'Comments', feature: true do include RepoHelpers include WaitForAjax - describe 'On merge requests page', feature: true do - it 'excludes award_emoji from comment count' do - merge_request = create(:merge_request) - project = merge_request.source_project - create(:upvote_note, noteable: merge_request, project: project) - - login_as :admin - visit namespace_project_merge_requests_path(project.namespace, project) - - expect(merge_request.mr_and_commit_notes.count).to eq 1 - expect(page.all('.merge-request-no-comments').first.text).to eq "0" - end - end - describe 'On a merge request', js: true, feature: true do let!(:project) { create(:project) } let!(:merge_request) do @@ -147,17 +133,6 @@ describe 'Comments', feature: true do end end end - - describe 'comment info' do - it 'excludes award_emoji from comment count' do - create(:upvote_note, noteable: merge_request, project: project) - - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - expect(merge_request.mr_and_commit_notes.count).to eq 2 - expect(find('.notes-tab span.badge').text).to eq "1" - end - end end describe 'On a merge request diff', js: true, feature: true do diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb index 54aa9c66a08..54aa9c66a08 100644 --- a/spec/features/project/shortcuts_spec.rb +++ b/spec/features/projects/shortcuts_spec.rb diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb index 72491ac7e61..32fa88a2b21 100644 --- a/spec/features/todos/target_state_spec.rb +++ b/spec/features/todos/target_state_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' feature 'Todo target states', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } before do login_as user diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 4e627753cc7..8e1833a069e 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:issue) { create(:issue) } describe 'GET /dashboard/todos' do @@ -49,7 +49,7 @@ describe 'Dashboard Todos', feature: true do note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project) create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id) - project2 = create(:project) + project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) label2 = create(:label, project: project2) issue2 = create(:issue, project: project2) note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2) @@ -98,5 +98,18 @@ describe 'Dashboard Todos', feature: true do end end end + + context 'User has a Todo in a project pending deletion' do + before do + deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path + end + + it 'shows "All done" message' do + expect(page).to have_content "You're all done!" + end + end end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb new file mode 100644 index 00000000000..366a90228b1 --- /dev/null +++ b/spec/features/u2f_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do + def register_u2f_device(u2f_device = nil) + u2f_device ||= FakeU2fDevice.new(page) + u2f_device.respond_to_u2f_registration + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + u2f_device + end + + describe "registration" do + let(:user) { create(:user) } + before { login_as(user) } + + describe 'when 2FA via OTP is disabled' do + it 'allows registering a new device' do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Enable Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + click_on 'Manage Two-Factor Authentication' + + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + describe 'when 2FA via OTP is enabled' do + before { user.update_attributes(otp_required_for_login: true) } + + it 'allows registering a new device' do + visit profile_account_path + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match("You've already enabled two-factor authentication using mobile") + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + it 'allows the same device to be registered for multiple users' do + # First user + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + u2f_device = register_u2f_device + expect(page.body).to match('Your U2F device was registered') + logout + + # Second user + login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(u2f_device) + expect(page.body).to match('Your U2F device was registered') + + expect(U2fRegistration.count).to eq(2) + end + + context "when there are form errors" do + it "doesn't register the device if there are errors" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Have the "u2f device" respond with bad data + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + + expect(U2fRegistration.count).to eq(0) + expect(page.body).to match("The form contains the following error") + expect(page.body).to match("did not send a valid JSON response") + end + + it "allows retrying registration" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Failed registration + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + expect(page.body).to match("The form contains the following error") + + # Successful registration + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + expect(U2fRegistration.count).to eq(1) + end + end + end + + describe "authentication" do + let(:user) { create(:user) } + + before do + # Register and logout + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + @u2f_device = register_u2f_device + logout + end + + describe "when 2FA via OTP is disabled" do + it "allows logging in with the U2F device" do + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when 2FA via OTP is enabled" do + it "allows logging in with the U2F device" do + user.update_attributes(otp_required_for_login: true) + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when a given U2F device has already been registered by another user" do + describe "but not the current user" do + it "does not allow logging in with that particular device" do + # Register current user with the different U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + logout + + # Try authenticating user with the old U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + + describe "and also the current user" do + it "allows logging in with that particular device" do + # Register current user with the same U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(@u2f_device) + logout + + # Try authenticating user with the same U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + end + + describe "when a given U2F device has not been registered" do + it "does not allow logging in with that particular device" do + unregistered_device = FakeU2fDevice.new(page) + login_as(user) + unregistered_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + end + + describe "when two-factor authentication is disabled" do + let(:user) { create(:user) } + + before do + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + end + + it "deletes u2f registrations" do + expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0) + end + end +end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index bffe2c18b6f..eae61a54dfc 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -163,18 +163,15 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "note_active_class" do - before do - @note = create :note - @note1 = create :note - end + describe '#award_active_class' do + let!(:upvote) { create(:award_emoji) } it "returns empty string for unauthenticated user" do - expect(note_active_class(Note.all, nil)).to eq("") + expect(award_active_class(AwardEmoji.all, nil)).to eq("") end it "returns active string for author" do - expect(note_active_class(Note.all, @note.author)).to eq("active") + expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active") end end diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml new file mode 100644 index 00000000000..859e79a6c9e --- /dev/null +++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml @@ -0,0 +1 @@ += render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" } diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml new file mode 100644 index 00000000000..393c0613fd3 --- /dev/null +++ b/spec/javascripts/fixtures/u2f/register.html.haml @@ -0,0 +1 @@ += render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' } diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 78d39f1b428..82ee1954a59 100644 --- a/spec/javascripts/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph_contributors_graph +//= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index dbafe782b77..56970e22e34 100644 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph_contributors_util +//= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { @@ -9,14 +9,14 @@ describe("ContributorsStatGraphUtil", function () { {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] - + var correct_parsed_log = { total: [ {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, @@ -132,8 +132,8 @@ describe("ContributorsStatGraphUtil", function () { total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author:[ - { - author: "Karlo Soriano", + { + author: "Karlo Soriano", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, { @@ -161,11 +161,11 @@ describe("ContributorsStatGraphUtil", function () { it("returns the log by author sorted by specified field", function () { var fake_parsed_log = { total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} ], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 4c652910cd6..4b05d401a42 100644 --- a/spec/javascripts/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph +//= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee new file mode 100644 index 00000000000..e8a2892d678 --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.coffee @@ -0,0 +1,52 @@ +#= require u2f/authenticate +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FAuthenticate', -> + U2FUtil.enableTestMode() + fixture.load('u2f/authenticate') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-authenticate-u2f") + @component = new U2FAuthenticate(@container, {}, "token") + @component.start() + + it 'allows authenticating via a U2F device', -> + setupButton = @container.find("#js-login-u2f-device") + setupMessage = @container.find("p") + expect(setupMessage.text()).toContain('Insert your security key') + expect(setupButton.text()).toBe('Login Via U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.find("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + deviceResponse = @container.find('#js-device-response') + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "displays an error message", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying authentication after an error", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + retryButton = @container.find("#js-u2f-try-again") + retryButton.trigger('click') + + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee new file mode 100644 index 00000000000..97ed0e83a0e --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee @@ -0,0 +1,15 @@ +class @MockU2FDevice + constructor: () -> + window.u2f ||= {} + + window.u2f.register = (appId, registerRequests, signRequests, callback) => + @registerCallback = callback + + window.u2f.sign = (appId, challenges, signRequests, callback) => + @authenticateCallback = callback + + respondToRegisterRequest: (params) => + @registerCallback(params) + + respondToAuthenticateRequest: (params) => + @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee new file mode 100644 index 00000000000..0858abeca1a --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js.coffee @@ -0,0 +1,57 @@ +#= require u2f/register +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FRegister', -> + U2FUtil.enableTestMode() + fixture.load('u2f/register') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-register-u2f") + @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") + @component.start() + + it 'allows registering a U2F device', -> + setupButton = @container.find("#js-setup-u2f-device") + expect(setupButton.text()).toBe('Setup New U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.children("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find('p') + deviceResponse = @container.find('#js-device-response') + expect(registeredMessage.text()).toContain("Your device was successfully set up!") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "doesn't allow the same device to be registered twice (for the same user", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: 4}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("already been registered with us") + + it "displays an error message for other errors", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying registration after an error", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + retryButton = @container.find("#U2FTryAgain") + retryButton.trigger('click') + + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find("p") + expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb new file mode 100644 index 00000000000..55e681f6faf --- /dev/null +++ b/spec/lib/banzai/filter/reference_filter_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Banzai::Filter::ReferenceFilter, lib: true do + let(:project) { build(:project) } + + describe '#each_node' do + it 'iterates over the nodes in a document' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }. + to yield_with_args(an_instance_of(Nokogiri::XML::Element)) + end + + it 'returns an Enumerator when no block is given' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect(filter.each_node).to be_an_instance_of(Enumerator) + end + + it 'skips links with a "gfm" class' do + document = Nokogiri::HTML.fragment('<a href="foo" class="gfm">foo</a>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + + it 'skips text nodes in pre elements' do + document = Nokogiri::HTML.fragment('<pre>foo</pre>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + end + + describe '#nodes' do + it 'returns an Array of the HTML nodes' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect(filter.nodes).to eq([document.children[0]]) + end + end +end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index d7dfd6699ef..108b36a97cc 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -136,4 +136,23 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end end + + describe '#namespaces' do + it 'returns a Hash containing all Namespaces' do + document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + filter = described_class.new(document, project: project) + ns = user.namespace + + expect(filter.namespaces).to eq({ ns.path => ns }) + end + end + + describe '#usernames' do + it 'returns the usernames mentioned in a document' do + document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + filter = described_class.new(document, project: project) + + expect(filter.usernames).to eq([user.username]) + end + end end diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb index c3098574292..0f3852b1729 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/gitlab/award_emoji_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe AwardEmoji do +describe Gitlab::AwardEmoji do describe '.urls' do - subject { AwardEmoji.urls } + subject { Gitlab::AwardEmoji.urls } it { is_expected.to be_an_instance_of(Array) } it { is_expected.not_to be_empty } @@ -19,7 +19,7 @@ describe AwardEmoji do describe '.emoji_by_category' do it "only contains known categories" do - undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys + undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys expect(undefined_categories).to be_empty end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b6f7a2e7ec4..6b2b335d4fc 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } - let!(:build) { create(:ci_build, commit: ci_commit) } - + let!(:build) { create_build(project, sha, branch) } context 'build success' do before { build.success! } @@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do end end + context 'when outdated pipeline for given ref exists' do + before do + build = create_build(project, sha, branch) + build.success! + + old_build = create_build(project, '11eeffdd', branch) + old_build.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.to_s).to eq 'build-success' + end + end + + def create_build(project, sha, branch) + ci_commit = create(:ci_commit, project: project, + sha: sha, + ref: branch) + + create(:ci_build, commit: ci_commit) + end + def status_node(data, status) xml = Nokogiri::XML.parse(data) xml.at(%Q{text:contains("#{status}")}) diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb new file mode 100644 index 00000000000..cb3c592f8cd --- /dev/null +++ b/spec/models/award_emoji_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe AwardEmoji, models: true do + describe 'Associations' do + it { is_expected.to belong_to(:awardable) } + it { is_expected.to belong_to(:user) } + end + + describe 'modules' do + it { is_expected.to include_module(Participable) } + end + + describe "validations" do + it { is_expected.to validate_presence_of(:awardable) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:name) } + + # To circumvent a bug in the shoulda matchers + describe "scoped uniqueness validation" do + it "rejects duplicate award emoji" do + user = create(:user) + issue = create(:issue) + create(:award_emoji, user: user, awardable: issue) + new_award = build(:award_emoji, user: user, awardable: issue) + + expect(new_award).not_to be_valid + end + end + end +end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index c712d211b0f..98f60087cf5 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -23,7 +23,7 @@ describe Ci::Variable, models: true do end it 'fails to decrypt if iv is incorrect' do - subject.encrypted_value_iv = nil + subject.encrypted_value_iv = SecureRandom.hex subject.instance_variable_set(:@value, nil) expect { subject.value }. to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 6bc496414a3..384a38ebc69 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -151,9 +151,10 @@ describe CommitRange, models: true do issue = create(:issue) create(:note_on_issue, - noteable_id: issue.id, + noteable: issue, system: true, - note: commit1.revert_description) + note: commit1.revert_description, + project: issue.project) expect_any_instance_of(Commit).to receive(:reverts_commit?). with(commit1). diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb new file mode 100644 index 00000000000..a371c4a18a9 --- /dev/null +++ b/spec/models/concerns/awardable_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Issue, "Awardable" do + let!(:issue) { create(:issue) } + let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) } + + describe "Associations" do + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } + end + + describe "ClassMethods" do + let!(:issue2) { create(:issue) } + + before do + create(:award_emoji, awardable: issue2) + end + + it "orders on upvotes" do + expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue] + end + + it "orders on downvotes" do + expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2] + end + end + + describe "#upvotes" do + it "counts the number of upvotes" do + expect(issue.upvotes).to be 0 + end + end + + describe "#downvotes" do + it "counts the number of downvotes" do + expect(issue.downvotes).to be 1 + end + end + + describe "#toggle_award_emoji" do + it "adds an emoji if it isn't awarded yet" do + expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1) + end + + it "toggles already awarded emoji" do + expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index fb20578d8d3..dd03d64f750 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -12,6 +12,10 @@ describe Issue, "Issuable" do it { is_expected.to have_many(:todos).dependent(:destroy) } end + describe 'Included modules' do + it { is_expected.to include_module(Awardable) } + end + describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) @@ -227,12 +231,26 @@ describe Issue, "Issuable" do end end + describe '#labels_array' do + let(:project) { create(:project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:issue) { create(:issue, project: project) } + + before(:each) do + issue.labels << bug + end + + it 'loads the association and returns it as an array' do + expect(issue.reload.labels_array).to eq([bug]) + end + end + describe "votes" do let(:project) { issue.project } before do - issue.notes.awards.create!(note: "thumbsup", author: user, project: project) - issue.notes.awards.create!(note: "thumbsdown", author: user, project: project) + create(:award_emoji, :upvote, awardable: issue) + create(:award_emoji, :downvote, awardable: issue) end it "returns correct values" do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 87b3d8d650a..b87d68283e6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -269,4 +269,21 @@ describe Issue, models: true do end end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + issue = create(:issue, assignee: user1) + + expect(user1.assigned_open_issues_count).to eq(1) + expect(user2.assigned_open_issues_count).to eq(0) + + issue.assignee = user2 + issue.save + + expect(user1.assigned_open_issues_count).to eq(0) + expect(user2.assigned_open_issues_count).to eq(1) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 118e1e22a78..a4c55cc2fd0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -438,4 +438,21 @@ describe MergeRequest, models: true do expect(mr.participants).to include(note1.author, note2.author) end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + mr = create(:merge_request, assignee: user1) + + expect(user1.assigned_open_merge_request_count).to eq(1) + expect(user2.assigned_open_merge_request_count).to eq(0) + + mr.assignee = user2 + mr.save + + expect(user1.assigned_open_merge_request_count).to eq(0) + expect(user2.assigned_open_merge_request_count).to eq(1) + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index b25150f7055..139f7cb9783 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -154,7 +154,7 @@ describe Note, models: true do context "confidential issues" do let(:user) { create :user } let(:confidential_issue) { create(:issue, :confidential, author: user) } - let(:confidential_note) { create :note, note: "Random", noteable: confidential_issue } + let(:confidential_note) { create :note, note: "Random", noteable: confidential_issue, project: confidential_issue.project } it "returns notes with matching content if user can see the issue" do expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) @@ -171,23 +171,6 @@ describe Note, models: true do end end - describe '.grouped_awards' do - before do - create :note, note: "smile", is_award: true - create :note, note: "smile", is_award: true - end - - it "returns grouped hash of notes" do - expect(Note.grouped_awards.keys.size).to eq(3) - expect(Note.grouped_awards["smile"]).to match_array(Note.all) - end - - it "returns thumbsup and thumbsdown always" do - expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none) - expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none) - end - end - describe "editable?" do it "returns true" do note = build(:note) @@ -198,11 +181,6 @@ describe Note, models: true do note = build(:note, system: true) expect(note.editable?).to be_falsy end - - it "returns false" do - note = build(:note, is_award: true, note: "smiley") - expect(note.editable?).to be_falsy - end end describe "cross_reference_not_visible_for?" do @@ -229,29 +207,6 @@ describe Note, models: true do end end - describe "set_award!" do - let(:merge_request) { create :merge_request } - - it "converts aliases to actual name" do - note = create(:note, note: ":+1:", - noteable: merge_request, - project: merge_request.project) - - expect(note.reload.note).to eq("thumbsup") - end - - it "is not an award emoji when comment is on a diff" do - note = create(:note_on_merge_request_diff, note: ":blowfish:", - noteable: merge_request, - project: merge_request.project, - line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") - note = note.reload - - expect(note.note).to eq(":blowfish:") - expect(note.is_award?).to be_falsy - end - end - describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6c1b0393682..338a4c3d3f0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -784,6 +784,15 @@ describe Project, models: true do end end + describe '#container_registry_path_with_namespace' do + let(:project) { create(:empty_project, path: 'PROJECT') } + + subject { project.container_registry_path_with_namespace } + + it { is_expected.not_to eq(project.path_with_namespace) } + it { is_expected.to eq(project.path_with_namespace.downcase) } + end + describe '#container_registry_repository' do let(:project) { create(:empty_project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 548bec364f8..6ea8bf9bbe1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -30,6 +30,7 @@ describe User, models: true do it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end describe 'validations' do @@ -120,6 +121,66 @@ describe User, models: true do end end + describe "scopes" do + describe ".with_two_factor" do + it "returns users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to eq([user_with_2fa.id]) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + end + + describe ".without_two_factor" do + it "excludes users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + end + end + describe "Respond to" do it { is_expected.to respond_to(:is_admin?) } it { is_expected.to respond_to(:name) } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 37ab9cc8cfe..bb926172593 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -249,7 +249,6 @@ describe API::API, api: true do expect(json_response['milestone']).to be_a Hash expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash - expect(json_response['user_notes_count']).to be(1) end it "should return a project issue by id" do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4b0111df149..10d22189c8d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -138,7 +138,6 @@ describe API::API, api: true do expect(json_response['work_in_progress']).to be_falsy expect(json_response['merge_when_build_succeeds']).to be_falsy expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['user_notes_count']).to be(2) end it "should return merge_request" do @@ -428,6 +427,19 @@ describe API::API, api: true do expect(json_response['message']).to eq('401 Unauthorized') end + it "returns 409 if the SHA parameter doesn't match" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ + + expect(response.status).to eq(409) + expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') + end + + it "succeeds if the SHA parameter matches" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha + + expect(response.status).to eq(200) + end + it "enables merge when build succeeds if the ci is active" do allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) allow(ci_commit).to receive(:active?).and_return(true) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 96f050bbd9b..4a689e64dc5 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,114 +1,265 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:issue) { create(:issue, project: @project) } - - before do - @user = create :user - opts = { - name: "GitLab", - namespace: @user.namespace - } - @project = Projects::CreateService.new(@user, opts).execute - end + let(:user) { create(:user) } + let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } - describe :close_issue do + let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } - before do - @issues = create_list(:issue, 5, project: @project) - @params = { + describe :close_issue do + let(:issues) { create_list(:issue, 5, project: project) } + let(:params) do + { state_event: 'close', - issues_ids: @issues.map(&:id) + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.opened).to be_empty - expect(@project.issues.closed).not_to be_empty + expect(result[:count]).to eq(issues.count) end + it 'closes all the issues passed' do + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + end end describe :reopen_issues do - before do - @issues = create_list(:closed_issue, 5, project: @project) - @params = { + let(:issues) { create_list(:closed_issue, 5, project: project) } + let(:params) do + { state_event: 'reopen', - issues_ids: @issues.map(&:id) + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.closed).to be_empty - expect(@project.issues.opened).not_to be_empty + expect(result[:count]).to eq(issues.count) end + it 'reopens all the issues passed' do + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty + end end - describe :update_assignee do + describe 'updating assignee' do + let(:issue) do + create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } + end - before do - @new_assignee = create :user - @params = { - issues_ids: [issue.id], - assignee_id: @new_assignee.id + let(:params) do + { + assignee_id: assignee_id, + issues_ids: issue.id.to_s } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) + context 'when the new assignee ID is a valid user' do + let(:new_assignee) { create(:user) } + let(:assignee_id) { new_assignee.id } - expect(@project.issues.first.assignee).to eq(@new_assignee) - end + it 'succeeds' do + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'allows mass-unassigning' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil + it 'updates the assignee to the use ID passed' do + expect(issue.reload.assignee).to eq(new_assignee) + end + end - @params[:assignee_id] = -1 + context 'when the new assignee ID is -1' do + let(:assignee_id) { -1 } - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).to be_nil + it 'unassigns the issues' do + expect(issue.reload.assignee).to be_nil + end end - it 'does not unassign when assignee_id is not present' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil + context 'when the new assignee ID is not present' do + let(:assignee_id) { nil } - @params[:assignee_id] = '' - - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).not_to be_nil + it 'does not unassign' do + expect(issue.reload.assignee).to eq(user) + end end end - describe :update_milestone do + describe 'updating milestones' do + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } - before do - @milestone = create(:milestone, project: @project) - @params = { - issues_ids: [issue.id], - milestone_id: @milestone.id + let(:params) do + { + issues_ids: issue.id.to_s, + milestone_id: milestone.id } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds' do expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) + end - expect(@project.issues.first.milestone).to eq(@milestone) + it 'updates the issue milestone' do + expect(project.issues.first.milestone).to eq(milestone) end end + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } + end + + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } + + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } + + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } + + let(:params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id), + issues_ids: issues.map(&:id).join(',') + } + end + + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [bug, regression] } + + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + + context 'when those label IDs are empty' do + let(:labels) { [] } + + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end + end + + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } + + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } + + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } + + it 'remove the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) + end + end + + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 95fe6c2400a..93bf0f64963 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do let!(:milestone2) do create(:milestone, project_id: new_project.id, title: 'v9.0') end + let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } let!(:new_issue) { move_service.execute(old_issue, new_project) } end @@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do it 'preserves create time' do expect(old_issue.created_at).to eq new_issue.created_at end + + it 'moves the award emoji' do + expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name + end end context 'issue with notes' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index be19be17151..dacbcd8fb46 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Issues::UpdateService, services: true do @@ -273,5 +274,50 @@ describe Issues::UpdateService, services: true do end end end + + context 'updating labels' do + let(:label3) { create(:label, project: project) } + let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload } + + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to include(label.id) + end + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label, label3]) } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to be_empty + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label]) } + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index ff23f13e1cb..35f576874b8 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - + @note = Notes::CreateService.new(project, user, opts).execute end @@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do project.team << [user, :master] end - it "creates emoji note" do + it "creates an award emoji" do opts = { note: ':smile: ', noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute - @note = Notes::CreateService.new(project, user, opts).execute - - expect(@note).to be_valid - expect(@note.note).to eq('smile') - expect(@note.is_award).to be_truthy + expect(note).to be_valid + expect(note.name).to eq('smile') end it "creates regular note if emoji name is invalid" do @@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute + + expect(note).to be_valid + expect(note.note).to eq(opts[:note]) + end + + it "normalizes the emoji name" do + opts = { + note: ':+1:', + noteable_type: 'Issue', + noteable_id: issue.id + } - @note = Notes::CreateService.new(project, user, opts).execute + expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) - expect(@note).to be_valid - expect(@note.note).to eq(opts[:note]) - expect(@note.is_award).to be_falsy + Notes::CreateService.new(project, user, opts).execute end end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f2dcdab960..9d90bfceb73 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -49,7 +49,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq 'Failed to import the repository' + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 42147736532..6e7ecbd39ba 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -156,7 +156,6 @@ describe TodoService, services: true do let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } - let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } let(:system_note) { create(:system_note, project: project, noteable: issue) } it 'mark related pending todos to the noteable for the note author as done' do @@ -169,13 +168,6 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end - it 'mark related pending todos to the noteable for the award note author as done' do - service.new_note(award_note, john_doe) - - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done - end - it 'does not mark related pending todos it is a system note' do service.new_note(system_note, john_doe) @@ -306,6 +298,15 @@ describe TodoService, services: true do end end + describe '#new_award_emoji' do + it 'marks related pending todos to the target for the user as done' do + todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author) + service.new_award_emoji(mr_assigned, john_doe) + + expect(todo.reload).to be_done + end + end + describe '#merge_request_build_failed' do it 'creates a pending todo for the merge request author' do service.merge_request_build_failed(mr_unassigned) diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb new file mode 100644 index 00000000000..553fe9f1fbc --- /dev/null +++ b/spec/support/fake_u2f_device.rb @@ -0,0 +1,36 @@ +class FakeU2fDevice + def initialize(page) + @page = page + end + + def respond_to_u2f_registration + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + + json_response = u2f_device(app_id).register_response(challenges[0]) + + @page.execute_script(" + u2f.register = function(appId, registerRequests, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + def respond_to_u2f_authentication + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + json_response = u2f_device(app_id).sign_response(challenges[0]) + + @page.execute_script(" + u2f.sign = function(appId, challenges, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + private + + def u2f_device(app_id) + @u2f_device ||= U2F::FakeU2F.new(app_id) + end +end diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee new file mode 100644 index 00000000000..584751af8ea --- /dev/null +++ b/vendor/assets/javascripts/task_list.js.coffee @@ -0,0 +1,258 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 GitHub, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# TaskList Behavior +# +#= provides tasklist:enabled +#= provides tasklist:disabled +#= provides tasklist:change +#= provides tasklist:changed +# +# +# Enables Task List update behavior. +# +# ### Example Markup +# +# <div class="js-task-list-container"> +# <ul class="task-list"> +# <li class="task-list-item"> +# <input type="checkbox" class="js-task-list-item-checkbox" disabled /> +# text +# </li> +# </ul> +# <form> +# <textarea class="js-task-list-field">- [ ] text</textarea> +# </form> +# </div> +# +# ### Specification +# +# TaskLists MUST be contained in a `(div).js-task-list-container`. +# +# TaskList Items SHOULD be an a list (`UL`/`OL`) element. +# +# Task list items MUST match `(input).task-list-item-checkbox` and MUST be +# `disabled` by default. +# +# TaskLists MUST have a `(textarea).js-task-list-field` form element whose +# `value` attribute is the source (Markdown) to be udpated. The source MUST +# follow the syntax guidelines. +# +# TaskList updates trigger `tasklist:change` events. If the change is +# successful, `tasklist:changed` is fired. The change can be canceled. +# +# jQuery is required. +# +# ### Methods +# +# `.taskList('enable')` or `.taskList()` +# +# Enables TaskList updates for the container. +# +# `.taskList('disable')` +# +# Disables TaskList updates for the container. +# +## ### Events +# +# `tasklist:enabled` +# +# Fired when the TaskList is enabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:disabled` +# +# Fired when the TaskList is disabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:change` +# +# Fired before the TaskList item change takes affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** Yes +# * **Target** `.js-task-list-field` +# +# `tasklist:changed` +# +# Fired once the TaskList item change has taken affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-field` +# +# ### NOTE +# +# Task list checkboxes are rendered as disabled by default because rendered +# user content is cached without regard for the viewer. + +incomplete = "[ ]" +complete = "[x]" + +# Escapes the String for regular expression matching. +escapePattern = (str) -> + str. + replace(/([\[\]])/g, "\\$1"). # escape square brackets + replace(/\s/, "\\s"). # match all white space + replace("x", "[xX]") # match all cases + +incompletePattern = /// + #{escapePattern(incomplete)} +/// +completePattern = /// + #{escapePattern(complete)} +/// + +# Pattern used to identify all task list items. +# Useful when you need iterate over all items. +itemPattern = /// + ^ + (?: # prefix, consisting of + \s* # optional leading whitespace + (?:>\s*)* # zero or more blockquotes + (?:[-+*]|(?:\d+\.)) # list item indicator + ) + \s* # optional whitespace prefix + ( # checkbox + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + \s+ # is followed by whitespace + (?! + \(.*?\) # is not part of a [foo](url) link + ) + (?= # and is followed by zero or more links + (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)* + (?:[^\[]|$) # and either a non-link or the end of the string + ) +/// + +# Used to filter out code fences from the source for comparison only. +# http://rubular.com/r/x5EwZVrloI +# Modified slightly due to issues with JS +codeFencesPattern = /// + ^`{3} # ``` + (?:\s*\w+)? # followed by optional language + [\S\s] # whitespace + .* # code + [\S\s] # whitespace + ^`{3}$ # ``` +///mg + +# Used to filter out potential mismatches (items not in lists). +# http://rubular.com/r/OInl6CiePy +itemsInParasPattern = /// + ^ + ( + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + .+ + $ +///g + +# Given the source text, updates the appropriate task list item to match the +# given checked value. +# +# Returns the updated String text. +updateTaskListItem = (source, itemIndex, checked) -> + clean = source.replace(/\r/g, '').replace(codeFencesPattern, ''). + replace(itemsInParasPattern, '').split("\n") + index = 0 + result = for line in source.split("\n") + if line in clean && line.match(itemPattern) + index += 1 + if index == itemIndex + line = + if checked + line.replace(incompletePattern, complete) + else + line.replace(completePattern, incomplete) + line + result.join("\n") + +# Updates the $field value to reflect the state of $item. +# Triggers the `tasklist:change` event before the value has changed, and fires +# a `tasklist:changed` event once the value has changed. +updateTaskList = ($item) -> + $container = $item.closest '.js-task-list-container' + $field = $container.find '.js-task-list-field' + index = 1 + $container.find('.task-list-item-checkbox').index($item) + checked = $item.prop 'checked' + + event = $.Event 'tasklist:change' + $field.trigger event, [index, checked] + + unless event.isDefaultPrevented() + $field.val updateTaskListItem($field.val(), index, checked) + $field.trigger 'change' + $field.trigger 'tasklist:changed', [index, checked] + +# When the task list item checkbox is updated, submit the change +$(document).on 'change', '.task-list-item-checkbox', -> + updateTaskList $(this) + +# Enables TaskList item changes. +enableTaskList = ($container) -> + if $container.find('.js-task-list-field').length > 0 + $container. + find('.task-list-item').addClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', null) + $container.addClass('is-task-list-enabled'). + trigger 'tasklist:enabled' + +# Enables a collection of TaskList containers. +enableTaskLists = ($containers) -> + for container in $containers + enableTaskList $(container) + +# Disable TaskList item changes. +disableTaskList = ($container) -> + $container. + find('.task-list-item').removeClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', 'disabled') + $container.removeClass('is-task-list-enabled'). + trigger 'tasklist:disabled' + +# Disables a collection of TaskList containers. +disableTaskLists = ($containers) -> + for container in $containers + disableTaskList $(container) + +$.fn.taskList = (method) -> + $container = $(this).closest('.js-task-list-container') + + methods = + enable: enableTaskLists + disable: disableTaskLists + + methods[method || 'enable']($container) diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js new file mode 100644 index 00000000000..e666b136051 --- /dev/null +++ b/vendor/assets/javascripts/u2f.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array<u2f.Transport>} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * |function((u2f.Error|u2f.SignResponse)))>} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.<u2f.Response>} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +};
\ No newline at end of file |