diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbdbae9d787d9ed2826b473a78d7fe513f82ec74..1c4d98ea3f677c9346eff93b1d8b45337a6c1b7f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,11 @@ services: - postgres:latest - redis:latest +cache: + key: "ruby22" + paths: + - vendor + variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" @@ -137,23 +142,145 @@ bundler:audit: # Ruby 2.1 jobs -spec:ruby21: +spec:feature:ruby21: image: ruby:2.1 + only: + - master script: - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature + cache: + key: "ruby21" + paths: + - vendor tags: - ruby - mysql + +spec:api:ruby21: + image: ruby:2.1 only: - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql -spinach:ruby21: +spec:models:ruby21: image: ruby:2.1 + only: + - master script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models + cache: + key: "ruby21" + paths: + - vendor tags: - ruby - mysql + +spec:lib:ruby21: + image: ruby:2.1 only: - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + +spec:services:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + +spec:benchmark:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test bundle exec rake spec:benchmark + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + allow_failure: true + +spec:other:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:project:half:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:project:rest:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + +spinach:other:ruby21: + image: ruby:2.1 + only: + - master + script: + - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other + cache: + key: "ruby21" + paths: + - vendor + tags: + - ruby + - mysql + diff --git a/CHANGELOG b/CHANGELOG index 988486e818d1b6e0455d7e2cabbf03322a7502b3..74bc366d2037bbb093ae29d92af0e518760c8cee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,45 +1,73 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.5.0 (unreleased) + - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu) + - Cache various Repository methods to improve performance (Yorick Peterse) + - Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu) - Ensure rake tasks that don't need a DB connection can be run without one - Update New Relic gem to 3.14.1.311 (Stan Hu) - Add "visibility" flag to GET /projects api endpoint + - Add an option to supply root email through an environmental variable (Koichiro Mikami) - Ignore binary files in code search to prevent Error 500 (Stan Hu) - Render sanitized SVG images (Stan Hu) - Support download access by PRIVATE-TOKEN header (Stan Hu) - Upgrade gitlab_git to 7.2.23 to fix commit message mentions in first branch push + - Add option to include the sender name in body of Notify email (Jason Lee) - New UI for pagination - Don't prevent sign out when 2FA enforcement is enabled and user hasn't yet set it up + - API: Added "merge_requests/:merge_request_id/closes_issues" (Gal Schlezinger) - Fix diff comments loaded by AJAX to load comment with diff in discussion tab + - Fix relative links in other markup formats (Ben Boeckel) - Whitelist raw "abbr" elements when parsing Markdown (Benedict Etzel) - Fix label links for a merge request pointing to issues list - Don't vendor minified JS + - Increase project import timeout to 15 minutes + - Be more permissive with email address validation: it only has to contain a single '@' - Display 404 error on group not found - Track project import failure - Support Two-factor Authentication for LDAP users - Display database type and version in Administration dashboard + - Allow limited Markdown in Broadcast Messages - Fix visibility level text in admin area (Zeger-Jan van de Weg) - Warn admin during OAuth of granting admin rights (Zeger-Jan van de Weg) - Update the ExternalIssue regex pattern (Blake Hitchcock) - Remember user's inline/side-by-side diff view preference in a cookie (Kirill Katsnelson) - Optimized performance of finding issues to be closed by a merge request + - Add `avatar_url`, `description`, `git_ssh_url`, `git_http_url`, `path_with_namespace` + and `default_branch` in `project` in push, issue, merge-request and note webhooks data (Kirill Zaitsev) + - Deprecate the `ssh_url` in favor of `git_ssh_url` and `http_url` in favor of `git_http_url` + in `project` for push, issue, merge-request and note webhooks data (Kirill Zaitsev) + - Deprecate the `repository` key in push, issue, merge-request and note webhooks data, use `project` instead (Kirill Zaitsev) + - API: Expose MergeRequest#merge_status (Andrei Dziahel) - Revert "Add IP check against DNSBLs at account sign-up" + - Actually use the `skip_merges` option in Repository#commits (Tony Chu) - Fix API to keep request parameters in Link header (Michael Potthoff) - Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead - Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead - Prevent parse error when name of project ends with .atom and prevent path issues - Mark inline difference between old and new paths when a file is renamed - Support Akismet spam checking for creation of issues via API (Stan Hu) + - API: Allow to set or update a merge-request's milestone (Kirill Skachkov) - Improve UI consistency between projects and groups lists - Add sort dropdown to dashboard projects page - Fixed logo animation on Safari (Roman Rott) - Hide remove source branch button when the MR is merged but new commits are pushed (Zeger-Jan van de Weg) - In seach autocomplete show only groups and projects you are member of + - Don't process cross-reference notes from forks - Fix: init.d script not working on OS X + - Faster snippet search + - Title for milestones should be unique (Zeger-Jan van de Weg) + - Validate correctness of maximum attachment size application setting + - Replaces "Create merge request" link with one to the "Merge Request" when one exists + - Fix CI builds badge, add a new link to builds badge, deprecate the old one + - Fix broken link to project in build notification emails v 8.4.4 - Update omniauth-saml gem to 1.4.2 + - Prevent long-running backup tasks from timing out the database connection + - Add a Project setting to allow guests to view build logs (defaults to true) + - Sort project milestones by due date including issue editor (Oliver Rogers / Orih) v 8.4.3 - Increase lfs_objects size column to 8-byte integer to allow files larger @@ -375,6 +403,7 @@ v 8.1.0 - Improved performance of the trending projects page - Remove CI migration task - Improved performance of finding projects by their namespace + - Add assignee data to Issuables' hook_data (Bram Daams) - Fix bug where transferring a project would result in stale commit links (Stan Hu) - Fix build trace updating - Include full path of source and target branch names in New Merge Request page (Stan Hu) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 263f98b1e57d3f794ec71b0009b0bb3db1f954e4..c4522998f42e9d6277b155c40659082f1e6704f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -364,6 +364,7 @@ corresponding merge request should be updated to have the following: This makes it easier for release managers to keep track of what still has to be merged and where changes have to be merged into. +Like all merge requests the target should be master so all bugfixes are in master. ## Definition of done diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 844f6a91acb92e5f4c58fe0d440fba8deea2a8c8..d2b13eb644d68d6a2962e3c6288f03ea32854725 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.6.3 +0.6.4 diff --git a/Gemfile b/Gemfile index 5b5f6aef77c5d7ed1cd4257aadb464218ffe3df9..87c150bd70441947bd9116ae80c728a3ce4a0dc2 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem "pg", '~> 0.18.2', group: :postgres gem 'devise', '~> 3.5.4' gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' -gem 'omniauth', '~> 1.2.2' +gem 'omniauth', '~> 1.3.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' @@ -108,7 +108,7 @@ gem 'rouge', '~> 1.10.1' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM -gem 'nokogiri', '1.6.7.2' +gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2' # Diffs gem 'diffy', '~> 3.0.3' diff --git a/Gemfile.lock b/Gemfile.lock index a1e36c304e892671ecfe26fca39dbf5bbdda577f..43c26396a30e3fe056c6a9093aea9e5bc807af7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -492,9 +492,9 @@ GEM rack (~> 1.2) octokit (3.8.0) sawyer (~> 0.6.0, >= 0.5.3) - omniauth (1.2.2) + omniauth (1.3.1) hashie (>= 1.2, < 4) - rack (~> 1.0) + rack (>= 1.0, < 3) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -961,11 +961,11 @@ DEPENDENCIES mysql2 (~> 0.3.16) nested_form (~> 0.3.2) net-ssh (~> 3.0.1) - nokogiri (= 1.6.7.2) + nokogiri (~> 1.6.7, >= 1.6.7.2) nprogress-rails (~> 0.1.6.7) oauth2 (~> 1.0.0) octokit (~> 3.8.0) - omniauth (~> 1.2.2) + omniauth (~> 1.3.1) omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee index eb951f717112e3160a876a0385a160832231b0ee..b2b8e1b7ffbef93d375c3917cde6cd7f46bd2d6c 100644 --- a/app/assets/javascripts/admin.js.coffee +++ b/app/assets/javascripts/admin.js.coffee @@ -12,19 +12,6 @@ class @Admin e.preventDefault() $('.js-toggle-colors-container').toggle() - $('input#broadcast_message_color').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('background-color', previewColor) - - $('input#broadcast_message_font').on 'input', -> - previewColor = $(@).val() - $('div.broadcast-message-preview').css('color', previewColor) - - $('textarea#broadcast_message_message').on 'input', -> - previewMessage = $(@).val() - previewMessage = "Your message here" if previewMessage.trim() == '' - $('div.broadcast-message-preview span').text(previewMessage) - $('.log-tabs a').click (e) -> e.preventDefault() $(this).tab('show') diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 047df4786a95641476fd4235268ffefb3ed630e9..360acb864f62e774426833fbef585227771be50b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -49,10 +49,11 @@ class @AwardsHandler counter.text(parseInt(counter.text()) - 1) emojiIcon.removeClass("active") @removeMeFromAuthorList(emoji) - else if emoji =="thumbsup" || emoji == "thumbsdown" + else if emoji == "thumbsup" || emoji == "thumbsdown" emojiIcon.tooltip("destroy") counter.text(0) emojiIcon.removeClass("active") + @removeMeFromAuthorList(emoji) else emojiIcon.tooltip("destroy") emojiIcon.remove() diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..a38a329c4c2345e32316a4f43cc635f3b4b85f52 --- /dev/null +++ b/app/assets/javascripts/broadcast_message.js.coffee @@ -0,0 +1,22 @@ +$ -> + $('input#broadcast_message_color').on 'input', -> + previewColor = $(@).val() + $('div.broadcast-message-preview').css('background-color', previewColor) + + $('input#broadcast_message_font').on 'input', -> + previewColor = $(@).val() + $('div.broadcast-message-preview').css('color', previewColor) + + previewPath = $('textarea#broadcast_message_message').data('preview-path') + + $('textarea#broadcast_message_message').on 'input', -> + message = $(@).val() + + if message == '' + $('.js-broadcast-message-preview').text("Your message here") + else + $.ajax( + url: previewPath + type: "POST" + data: { broadcast_message: { message: message } } + ) diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index dd295088312efedc45e0b3aca819a6c84bb18189..62143e66cfe614699d005637c45aa1708154e153 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,10 +1,11 @@ @Dashboard = init: -> + $(".projects-list-filter").off('keyup') this.initSearch() initSearch: -> @timer = null - $("#project-filter-form-field").on('keyup', -> + $(".projects-list-filter").on('keyup', -> clearTimeout(@timer) @timer = setTimeout(Dashboard.filterResults, 500) ) @@ -13,8 +14,8 @@ $('.projects-list-holder').fadeTo(250, 0.5) form = null - form = $("#project-filter-form") - search = $("#project-filter-form-field").val() + form = $("form#project-filter-form") + search = $(".projects-list-filter").val() project_filter_url = form.attr('action') + '?' + form.serialize() $.ajax @@ -24,7 +25,7 @@ complete: -> $('.projects-list-holder').fadeTo(250, 1) success: (data) -> - $('div.projects-list-holder').replaceWith(data.html) + $('.projects-list-holder').replaceWith(data.html) # Change url so if user reload a page - search results are saved history.replaceState {page: project_filter_url}, document.title, project_filter_url dataType: "json" diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index d4a2b74b1438b3a44c210f939af2cfe5d3368c38..b17f8e51470197c709050196e978b9e4e256a73d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -16,6 +16,8 @@ class Dispatcher shortcut_handler = null switch page + when 'explore:projects:index', 'explore:projects:starred', 'explore:projects:trending' + Dashboard.init() when 'projects:issues:index' Issues.init() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index e7d884662eae84aa04b06c33cfe5194b66280112..35b2fbbba0736d135b3426cbfc60e6896894c514 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -44,6 +44,7 @@ $(document).on('page:fetch', start) $(document).on('page:change', stop) $ -> - # Make logo clickable + # Make logo clickable as part of a workaround for Safari visited + # link behaviour (See !2690). $('#logo').on 'click', -> $('#js-shortcuts-home').get(0).click() diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index ebf7140b7e3d8d85648988c5df5c6ea730186ca7..eab34be652a711b85c136ce2c21e3be54fa9e74b 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -3,22 +3,24 @@ class @ProjectsList $(".projects-list .js-expand").on 'click', (e) -> e.preventDefault() list = $(this).closest('.projects-list') - list.find("li").show() - list.find("li.bottom").hide() - $(".projects-list-filter").keyup -> - terms = $(this).val() - uiBox = $('div.projects-list-holder') - filterSelector = $(this).data('filter-selector') || 'span.filter-title' + $("#filter_projects").on 'keyup', -> + ProjectsList.filter_results($("#filter_projects")) - if terms == "" || terms == undefined - uiBox.find("ul.projects-list li").show() - else - uiBox.find("ul.projects-list li").each (index) -> - name = $(this).find(filterSelector).text() + @filter_results: ($element) -> + terms = $element.val() + filterSelector = $element.data('filter-selector') || 'span.filter-title' - if name.toLowerCase().search(terms.toLowerCase()) == -1 - $(this).hide() - else - $(this).show() - uiBox.find("ul.projects-list li.bottom").hide() + if not terms + $(".projects-list li").show() + $('.gl-pagination').show() + else + $(".projects-list li").each (index) -> + $this = $(this) + name = $this.find(filterSelector).text() + + if name.toLowerCase().indexOf(terms.toLowerCase()) == -1 + $this.hide() + else + $this.show() + $('.gl-pagination').hide() diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 144852e787496222c9286ffae5cbab5650967aea..a61161810a3cbd69baf9fcaa44f29f33b84df0c0 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -55,6 +55,16 @@ @extend .alert-warning; padding: 10px; text-align: center; + + > div, p { + display: inline; + margin: 0; + + a { + color: inherit; + text-decoration: underline; + } + } } .broadcast-message-preview { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9d5dc42b6cc2685ca7941517b65822959dbc0232..ef62f069dc289d9521940d7d2fd2e248b8b3c270 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -80,6 +80,10 @@ display: inline-block; } + .select2-container span { + margin-top: 0; + } + .issuable-count { } @@ -88,6 +92,10 @@ margin-left: 20px; border-left: 1px solid $border-gray-light; padding-left: 10px; + + &:hover { + color: $gray-darkest; + } } } @@ -192,6 +200,10 @@ .btn { background: $gray-normal; border: 1px solid $border-gray-normal; + &:hover { + background: $gray-dark; + border: 1px solid $border-gray-dark; + } } &.right-sidebar-collapsed { @@ -223,6 +235,19 @@ display: block; margin-top: 0; } + + .btn-clipboard { + border: none; + + &:hover { + background: transparent; + } + + i { + color: #999999; + } + } + } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 1515086b16d96ff402898649eb604817b63779f8..04a99d8c84a288fadc1e095d759bfeffabed754b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -81,6 +81,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :sentry_dsn, :akismet_enabled, :akismet_api_key, + :email_author_in_body, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index a470d8654081b2a55fe8b5a0f3902bdff0b09aac..fc342924987237335e009368576e8ccc4f167a97 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -36,6 +36,10 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end end + def preview + @message = broadcast_message_params[:message] + end + protected def finder diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 48b1f95acb9a100b8f721252ee044c3fda74666f..2c329b60a19f376bcafe2d7a296f98eea1ac2a77 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -164,7 +164,7 @@ class ApplicationController < ActionController::Base end def git_not_found! - render html: "errors/git_not_found", layout: "errors", status: 404 + render "errors/git_not_found.html", layout: "errors", status: 404 end def method_missing(method_sym, *arguments, &block) diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index c420b59c3a20708d3000b00d24c69c9c5f6263db..5bb7d499cdc5182fc8b988fa03008c4ec3930021 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -3,52 +3,5 @@ module Ci def self.railtie_helpers_paths "app/helpers/ci" end - - private - - def authorize_access_project! - unless can?(current_user, :read_project, project) - return page_404 - end - end - - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return page_404 - end - end - - def authenticate_admin! - return render_404 unless current_user.is_admin? - end - - def authorize_manage_project! - unless can?(current_user, :admin_project, project) - return page_404 - end - end - - def page_404 - render file: "#{Rails.root}/public/404.html", status: 404, layout: false - end - - def default_headers - headers['X-Frame-Options'] = 'DENY' - headers['X-XSS-Protection'] = '1; mode=block' - end - - # JSON for infinite scroll via Pager object - def pager_json(partial, count) - html = render_to_string( - partial, - layout: false, - formats: [:html] - ) - - render json: { - html: html, - count: count - } - end end end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 3004c2d27f05b9c7fa5d1fa3b23338e3eeab0748..d1824b481d7b3a76d0b1c6446f9fc8fad47f62a6 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,8 +1,7 @@ module Ci class ProjectsController < Ci::ApplicationController - before_action :project, except: [:index] - before_action :authenticate_user!, except: [:index, :build, :badge] - before_action :authorize_access_project!, except: [:index, :badge] + before_action :project + before_action :authorize_read_project!, except: [:badge] before_action :no_cache, only: [:badge] protect_from_forgery @@ -13,9 +12,13 @@ module Ci # Project status badge # Image with build status for sha or ref + # + # This action in DEPRECATED, this is here only for backwards compatibility + # with projects migrated from GitLab CI. + # def badge + return render_404 unless @project image = Ci::ImageForBuildService.new.execute(@project, params) - send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 0bcc78a8bc798f9c8f8ecd7e931bbd99a10a65e6..2df6924b13d8d4d91576ef46fd65a5e067f66b46 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -12,7 +12,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.search(terms) end - @projects = @projects.page(params[:page]).per(PER_PAGE) + @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push respond_to do |format| @@ -41,7 +41,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.search(terms) end - @projects = @projects.page(params[:page]).per(PER_PAGE) + @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push @groups = [] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 2689bf4f1ec8f52be662a4286c310fdef026cea3..a384f3004db6ca9c24c6d1e8283b817edb80e563 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -6,19 +6,49 @@ class Explore::ProjectsController < Explore::ApplicationController @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.non_archived @projects = @projects.search(params[:search]) if params[:search].present? + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) + @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end def trending @projects = TrendingProjectsFinder.new.execute(current_user) @projects = @projects.non_archived - @projects = @projects.page(params[:page]).per(PER_PAGE) + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end def starred @projects = ProjectsFinder.new.execute(current_user) + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? @projects = @projects.reorder('star_count DESC') - @projects = @projects.page(params[:page]).per(PER_PAGE) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } + end + end end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 90475c17c170e2672b5182627290b50e45700256..ca5ce1e204650848cc33d8869ec34921c9a05f85 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -14,7 +14,7 @@ class GroupsController < Groups::ApplicationController # Load group projects before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] - before_action :event_filter, only: :show + before_action :event_filter, only: [:show, :events] layout :determine_layout @@ -41,14 +41,16 @@ class GroupsController < Groups::ApplicationController def show @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) - @projects = @projects.page(params[:page]).per(PER_PAGE) + @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| format.html format.json do - load_events - pager_json("events/_events", @events.count) + render json: { + html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + } end format.atom do @@ -58,6 +60,15 @@ class GroupsController < Groups::ApplicationController end end + def events + respond_to do |format| + format.json do + load_events + pager_json("events/_events", @events.count) + end + end + end + def edit end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f159a6d6dc68270194450328fac3320a9733f0e3..cfea12665163db0f29e8e24e36b4812f10df0642 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,6 +1,6 @@ class Projects::ArtifactsController < Projects::ApplicationController layout 'project' - before_action :authorize_read_build_artifacts! + before_action :authorize_read_build! def download unless artifacts_file.file_storage? @@ -43,14 +43,4 @@ class Projects::ArtifactsController < Projects::ApplicationController def artifacts_file @artifacts_file ||= build.artifacts_file end - - def authorize_read_build_artifacts! - unless can?(current_user, :read_build_artifacts, @project) - if current_user.nil? - return authenticate_user! - else - return render_404 - end - end - end end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..a4dd94b941c2ac2499d4b66f4d1afa1e388ad104 --- /dev/null +++ b/app/controllers/projects/badges_controller.rb @@ -0,0 +1,11 @@ +class Projects::BadgesController < Projects::ApplicationController + def build + respond_to do |format| + format.html { render_404 } + format.svg do + image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) + send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml') + end + end + end +end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 92d9699fe849b38267da9eca7c57464c0c9b8f85..ec379c53b8f6a5ed297c8058ca98a16631cea787 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,9 +1,8 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] - - before_action :authorize_manage_builds!, except: [:index, :show, :status] - - layout "project" + before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] + before_action :authorize_update_build!, except: [:index, :show, :status] + layout 'project' def index @scope = params[:scope] @@ -23,7 +22,6 @@ class Projects::BuildsController < Projects::ApplicationController def cancel_all @project.builds.running_or_pending.each(&:cancel) - redirect_to namespace_project_builds_path(project.namespace, project) end @@ -46,20 +44,18 @@ class Projects::BuildsController < Projects::ApplicationController end build = Ci::Build.retry(@build) - redirect_to build_path(build) end - def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) - end - def cancel @build.cancel - redirect_to build_path(@build) end + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + private def build @@ -69,10 +65,4 @@ class Projects::BuildsController < Projects::ApplicationController def build_path(build) namespace_project_build_path(build.project.namespace, build.project, build) end - - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return render_404 - end - end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 5a084e123a15171dac1ecbd1ebdbae3959ce0925..36951b9137264f403b6ba3cc178853266087a683 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -4,15 +4,13 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code!, except: [:cancel_builds] - before_action :authorize_manage_builds!, only: [:cancel_builds] + before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] + before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds] + before_action :authorize_read_commit_status!, only: [:builds] before_action :commit - before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds] before_action :define_show_vars, only: [:show, :builds] def show - return git_not_found! unless @commit - apply_diff_view_cookie! @line_notes = commit.notes.inline @@ -68,6 +66,8 @@ class Projects::CommitController < Projects::ApplicationController end def define_show_vars + return git_not_found! unless commit + if params[:w].to_i == 1 @diffs = commit.diffs({ ignore_whitespace_change: true }) else @@ -79,10 +79,4 @@ class Projects::CommitController < Projects::ApplicationController @statuses = ci_commit.statuses if ci_commit end - - def authorize_manage_builds! - unless can?(current_user, :manage_builds, project) - return render_404 - end - end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index bf5b54c8cb75925b586a5edf030adeb379e97a15..1420b96840cbf5c79e13034d87acc9ba69fdaf16 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,6 +21,9 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count + @merge_request = @project.merge_requests.opened. + find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) + respond_to do |format| format.html format.json { pager_json("projects/commits/_commits", @commits.size) } diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7bbe75b39740f953e20be316c097f2fa11b34a39..dc5d217f3e4ebb83df851c969db293bd205705f1 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -4,24 +4,23 @@ class Projects::CompareController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! + before_action :assign_ref_vars, only: [:index, :show] + before_action :merge_request, only: [:index, :show] def index - @ref = Addressable::URI.unescape(params[:to]) end def show - base_ref = Addressable::URI.unescape(params[:from]) - @ref = head_ref = Addressable::URI.unescape(params[:to]) diff_options = { ignore_whitespace_change: true } if params[:w] == '1' compare_result = CompareService.new. - execute(@project, head_ref, @project, base_ref, diff_options) + execute(@project, @head_ref, @project, @base_ref, diff_options) if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @commit = @project.commit(head_ref) - @base_commit = @project.merge_base_commit(base_ref, head_ref) + @commit = @project.commit(@head_ref) + @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diff_refs = [@base_commit, @commit] @line_notes = [] end @@ -31,4 +30,16 @@ class Projects::CompareController < Projects::ApplicationController redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) end + + private + + def assign_ref_vars + @base_ref = Addressable::URI.unescape(params[:from]) + @ref = @head_ref = Addressable::URI.unescape(params[:to]) + end + + def merge_request + @merge_request ||= @project.merge_requests.opened. + find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref) + end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 15506bd677ad0200c73cad9127bfdf0b2015ad75..a5c4ef1c7c744e75a36d6f94c8b653b2ef84b29f 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -11,11 +11,12 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to :html def index - @milestones = case params[:state] - when 'all'; @project.milestones.order("state, due_date DESC") - when 'closed'; @project.milestones.closed.order("due_date DESC") - else @project.milestones.active.order("due_date ASC") - end + @milestones = + case params[:state] + when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc) + when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc) + else @project.milestones.active.reorder(due_date: :asc, title: :asc) + end @milestones = @milestones.includes(:project) @milestones = @milestones.page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index e2785caa2fb0301c9a6b839d0af4a9857972ab75..bedeb4a295cf7d6e83e27d5b74af87c8f2fdeff0 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -1,5 +1,5 @@ class Projects::RunnerProjectsController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 4993b2648a55981a5bbe0523c4c4e766983162b7..0dd2d6a99bec9cc2ee7ce842f57c3eb4a6d81d00 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,6 +1,6 @@ class Projects::RunnersController < Projects::ApplicationController + before_action :authorize_admin_build! before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] - before_action :authorize_admin_project! layout 'project_settings' diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 30adfad1daa9f7fad7f97988d2182273a1411c0a..92359745cec883d6d9405340a5d9f571243529c8 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,5 +1,5 @@ class Projects::TriggersController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 10efafea9db71657ba77d15e7e9cfe40e0d4d1d9..002346545780385f108dd1b6247b3310080df493 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,5 +1,5 @@ class Projects::VariablesController < Projects::ApplicationController - before_action :authorize_admin_project! + before_action :authorize_admin_build! layout 'project_settings' diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 4df5095bd94735cf45ae140b03fc92e1a525833e..14ca7426c2f10b786491cd15045437e72fa051d9 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -227,6 +227,7 @@ class ProjectsController < ApplicationController :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, + :public_builds, ) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 622cbfe3cc49be25097828e9ebb97bb7d0bbfe28..ecefa9b006df1ed2cd4b7a06dee4125509708f52 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -212,8 +212,7 @@ module ApplicationHelper file_content end else - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + other_markup(file_name, file_content) end rescue RuntimeError simple_format(file_content) @@ -281,76 +280,6 @@ module ApplicationHelper end end - def issuable_link_next(project,issuable) - if project.nil? - nil - elsif current_controller?(:issues) - namespace_project_issue_path(project.namespace, project, next_issuable_for(project, issuable.id).try(:iid)) - elsif current_controller?(:merge_requests) - namespace_project_merge_request_path(project.namespace, project, next_issuable_for(project, issuable.id).try(:iid)) - end - end - - def issuable_link_prev(project,issuable) - if project.nil? - nil - elsif current_controller?(:issues) - namespace_project_issue_path(project.namespace, project, prev_issuable_for(project, issuable.id).try(:iid)) - elsif current_controller?(:merge_requests) - namespace_project_merge_request_path(project.namespace, project, prev_issuable_for(project, issuable.id).try(:iid)) - end - end - - def issuable_count(entity, project) - if project.nil? - 0 - elsif current_controller?(:issues) - project.issues.send(entity).count - elsif current_controller?(:merge_requests) - project.merge_requests.send(entity).count - end - end - - def next_issuable_for(project, id) - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.where("id > ?", id).last - elsif current_controller?(:merge_requests) - project.merge_requests.where("id > ?", id).last - end - end - - def has_next_issuable?(project, id) - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.where("id > ?", id).last - elsif current_controller?(:merge_requests) - project.merge_requests.where("id > ?", id).last - end - end - - def prev_issuable_for(project, id) - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.where("id < ?", id).first - elsif current_controller?(:merge_requests) - project.merge_requests.where("id < ?", id).first - end - end - - def has_prev_issuable?(project, id) - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.where("id < ?", id).first - elsif current_controller?(:merge_requests) - project.merge_requests.where("id < ?", id).first - end - end - def state_filters_text_for(entity, project) titles = { opened: "Open" diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 1ed8c710f771566dd6ba3b5511911a16704dd93f..43a29c96bcaab292cd74c0c89fa1480836fd193e 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -3,7 +3,7 @@ module BroadcastMessagesHelper return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do - icon('bullhorn') << ' ' << message.message + icon('bullhorn') << ' ' << render_broadcast_message(message.message) end end @@ -31,4 +31,8 @@ module BroadcastMessagesHelper 'Pending' end end + + def render_broadcast_message(message) + Banzai.render(message, pipeline: :broadcast_message).html_safe + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a226252251beb9c281105da01132fda7dbe1ce7..89d2a648494f644b9c48ca31798a5fbe415f30fc 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -78,6 +78,21 @@ module GitlabMarkdownHelper ) end + def other_markup(file_name, text) + Gitlab::OtherMarkup.render( + file_name, + text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref, + commit: @commit + ) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..91a3aa371ef541042d35c18a926d1151bd8408ae --- /dev/null +++ b/app/helpers/issuables_helper.rb @@ -0,0 +1,37 @@ +module IssuablesHelper + + def sidebar_gutter_toggle_icon + sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right') + end + + def sidebar_gutter_collapsed_class + "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" + end + + def issuables_count(issuable) + base_issuable_scope(issuable).maximum(:iid) + end + + def next_issuable_for(issuable) + base_issuable_scope(issuable).where('iid > ?', issuable.iid).last + end + + def prev_issuable_for(issuable) + base_issuable_scope(issuable).where('iid < ?', issuable.iid).first + end + + private + + def sidebar_gutter_collapsed? + cookies[:collapsed_gutter] == 'true' + end + + def base_issuable_scope(issuable) + issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) + end + + def issuable_state_scope(issuable) + issuable.open? ? :opened : :closed + end + +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 43262d579e980a32e325571e75746b7e2e893b83..ae4ebc0854a9e03aa7999f5cbe982177e536a960 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,14 +44,14 @@ module IssuesHelper end def bulk_update_milestone_options - milestones = project_active_milestones.to_a + milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end def milestone_options(object) - milestones = object.project.milestones.active.to_a + milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) @@ -69,7 +69,7 @@ module IssuesHelper end end - def issue_button_visibility(issue, closed) + def issue_button_visibility(issue, closed) return 'hidden' if issue.closed? == closed end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 75f2ed5e0541fb2b37e709ee932ab37d2cba19a6..29cb753e62cf1dcc8049ea2d974c514fb3b3cf68 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -3,18 +3,6 @@ module NavHelper cookies[:collapsed_nav] == 'true' end - def sidebar_gutter_collapsed_class - if cookies[:collapsed_gutter] == 'true' - "right-sidebar-collapsed" - else - "right-sidebar-expanded" - end - end - - def sidebar_gutter_collapsed? - cookies[:collapsed_gutter] == 'true' - end - def nav_sidebar_class if nav_menu_collapsed? "sidebar-collapsed" @@ -32,9 +20,9 @@ module NavHelper end def page_gutter_class - if current_path?('merge_requests#show') || - current_path?('merge_requests#diffs') || - current_path?('merge_requests#commits') || + if current_path?('merge_requests#show') || + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || current_path?('issues#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index dd49283089d1955fb5ed481f2ecb60f56ba0f3f8..d6fb629b0c216daf22e83a869f67c7764116581e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -98,10 +98,6 @@ module ProjectsHelper project_nav_tabs.include? name end - def project_active_milestones - @project.milestones.active.order("due_date, title ASC") - end - def project_for_deploy_key(deploy_key) if deploy_key.projects.include?(@project) @project @@ -141,7 +137,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if project.builds_enabled? && can?(current_user, :read_build, project) + if can?(current_user, :read_build, project) nav_tabs << :builds end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index bc36434f5492c7250e72b82cc72175d225a97dba..41ae404899294bf32a5a312eb574170d6acd3cc6 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -33,7 +33,7 @@ module SnippetsHelper # surrounding code. # # @returns Array, unique and sorted. - def matching_lines(lined_content, surrounding_lines) + def matching_lines(lined_content, surrounding_lines, query) used_lines = [] lined_content.each_with_index do |line, line_number| used_lines.concat bounded_line_numbers( @@ -51,9 +51,9 @@ module SnippetsHelper # surrounding_lines() worth of unmatching lines. # # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} - def chunk_snippet(snippet, surrounding_lines = 3) + def chunk_snippet(snippet, query, surrounding_lines = 3) lined_content = snippet.content.split("\n") - used_lines = matching_lines(lined_content, surrounding_lines) + used_lines = matching_lines(lined_content, surrounding_lines, query) snippet_chunk = [] snippet_chunks = [] diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb index 64c1ce8cfabfb8a0d6632f9a2d6fbc75305dd151..2f86d1be57625ee969cebef8b1e15c70b862ad91 100644 --- a/app/mailers/emails/builds.rb +++ b/app/mailers/emails/builds.rb @@ -3,26 +3,27 @@ module Emails def build_fail_email(build_id, to) @build = Ci::Build.find(build_id) @project = @build.project + add_project_headers - add_build_headers - headers['X-GitLab-Build-Status'] = "failed" + add_build_headers('failed') mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) end def build_success_email(build_id, to) @build = Ci::Build.find(build_id) @project = @build.project + add_project_headers - add_build_headers - headers['X-GitLab-Build-Status'] = "success" + add_build_headers('success') mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) end private - def add_build_headers + + def add_build_headers(status) headers['X-GitLab-Build-Id'] = @build.id headers['X-GitLab-Build-Ref'] = @build.ref + headers['X-GitLab-Build-Status'] = status.to_s end - end end diff --git a/app/models/ability.rb b/app/models/ability.rb index ab59a3506a29943cc2dc2644037e7fdcc8adf530..a866eadeebb55704edc476b2d825c288a8bf6b68 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,17 +5,18 @@ class Ability return [] unless user.is_a?(User) return [] if user.blocked? - case subject.class.name - when "Project" then project_abilities(user, subject) - when "Issue" then issue_abilities(user, subject) - when "Note" then note_abilities(user, subject) - when "ProjectSnippet" then project_snippet_abilities(user, subject) - when "PersonalSnippet" then personal_snippet_abilities(user, subject) - when "MergeRequest" then merge_request_abilities(user, subject) - when "Group" then group_abilities(user, subject) - when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then group_member_abilities(user, subject) - when "ProjectMember" then project_member_abilities(user, subject) + case subject + when CommitStatus then commit_status_abilities(user, subject) + when Project then project_abilities(user, subject) + when Issue then issue_abilities(user, subject) + when Note then note_abilities(user, subject) + when ProjectSnippet then project_snippet_abilities(user, subject) + when PersonalSnippet then personal_snippet_abilities(user, subject) + when MergeRequest then merge_request_abilities(user, subject) + when Group then group_abilities(user, subject) + when Namespace then namespace_abilities(user, subject) + when GroupMember then group_member_abilities(user, subject) + when ProjectMember then project_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -25,6 +26,8 @@ class Ability case true when subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) + when subject.is_a?(CommitStatus) + anonymous_commit_status_abilities(subject) when subject.is_a?(Project) || subject.respond_to?(:project) anonymous_project_abilities(subject) when subject.is_a?(Group) || subject.respond_to?(:group) @@ -52,16 +55,26 @@ class Ability :read_project_member, :read_merge_request, :read_note, - :read_build, + :read_commit_status, :download_code ] + # Allow to read builds by anonymous user if guests are allowed + rules << :read_build if project.public_builds? + rules - project_disabled_features_rules(project) else [] end end + def anonymous_commit_status_abilities(subject) + rules = anonymous_project_abilities(subject.project) + # If subject is Ci::Build which inherits from CommitStatus filter the abilities + rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) + rules + end + def anonymous_group_abilities(subject) group = if subject.is_a?(Group) subject @@ -113,6 +126,9 @@ class Ability if project.public? || project.internal? rules.push(*public_project_rules) + + # Allow to read builds for internal projects + rules << :read_build if project.public_builds? end if project.owner == user || user.admin? @@ -134,7 +150,8 @@ class Ability def public_project_rules @public_project_rules ||= project_guest_rules + [ :download_code, - :fork_project + :fork_project, + :read_commit_status, ] end @@ -149,7 +166,6 @@ class Ability :read_project_member, :read_merge_request, :read_note, - :read_build, :create_project, :create_issue, :create_note @@ -158,24 +174,26 @@ class Ability def project_report_rules @project_report_rules ||= project_guest_rules + [ - :create_commit_status, - :read_commit_statuses, - :read_build_artifacts, :download_code, :fork_project, :create_project_snippet, :update_issue, :admin_issue, - :admin_label + :admin_label, + :read_commit_status, + :read_build, ] end def project_dev_rules @project_dev_rules ||= project_report_rules + [ :admin_merge_request, + :create_commit_status, + :update_commit_status, + :create_build, + :update_build, :create_merge_request, :create_wiki, - :manage_builds, :push_code ] end @@ -201,7 +219,9 @@ class Ability :admin_merge_request, :admin_note, :admin_wiki, - :admin_project + :admin_project, + :admin_commit_status, + :admin_build ] end @@ -240,6 +260,10 @@ class Ability rules += named_abilities('wiki') end + unless project.builds_enabled + rules += named_abilities('build') + end + rules end @@ -376,6 +400,22 @@ class Ability rules end + def commit_status_abilities(user, subject) + rules = project_abilities(user, subject.project) + # If subject is Ci::Build which inherits from CommitStatus filter the abilities + rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build) + rules + end + + def filter_build_abilities(rules) + # If we can't read build we should also not have that + # ability when looking at this in context of commit_status + %w(read create update admin).each do |rule| + rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build") + end + rules + end + def abilities @abilities ||= begin abilities = Six.new diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9cafc78f7616e69d2048a80a7fde19c3de996444..269056e0e7739a408ce7637861113b8286428d37 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -43,6 +43,7 @@ # metrics_port :integer default(8089) # sentry_enabled :boolean default(FALSE) # sentry_dsn :string +# email_author_in_body :boolean default(FALSE) # class ApplicationSetting < ActiveRecord::Base @@ -70,8 +71,8 @@ class ApplicationSetting < ActiveRecord::Base url: true validates :admin_notification_email, - allow_blank: true, - email: true + email: true, + allow_blank: true validates :two_factor_grace_period, numericality: { greater_than_or_equal_to: 0 } @@ -92,6 +93,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :akismet_enabled + validates :max_attachment_size, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 04650a9e67a14d0374098b0bf7653c8e953f4ac2..5136d0196a5f71f72c31b06ae5d2fe0811d2e5c8 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -126,17 +126,17 @@ module Issuable end def to_hook_data(user) - { + hook_data = { object_kind: self.class.name.underscore, user: user.hook_attrs, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url - }, - object_attributes: hook_attrs + project: project.hook_attrs, + object_attributes: hook_attrs, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } + hook_data.merge!(assignee: assignee.hook_attrs) if assignee + + hook_data end def label_names diff --git a/app/models/email.rb b/app/models/email.rb index 935705e2ed44b2b19372c7f57ab6980687e157ed..b323d1edd100c53d8fb58a838732959c8396ba63 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -15,7 +15,7 @@ class Email < ActiveRecord::Base belongs_to :user validates :user_id, presence: true - validates :email, presence: true, email: { strict_mode: true }, uniqueness: true + validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } before_validation :cleanup_email diff --git a/app/models/member.rb b/app/models/member.rb index 34efcd0088d006722958d0ff2c0003bcb4935cf2..ca08007b7eb9ef032f673589dd348a9aa2f43e87 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -39,7 +39,6 @@ class Member < ActiveRecord::Base if: :invite? }, email: { - strict_mode: true, allow_nil: true }, uniqueness: { diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ddc476447cab0dd570fa7049c6a0e96a307a84d4..1be8061e53d1c7547954802a8f85a07887b03882 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,6 +137,7 @@ class MergeRequest < ActiveRecord::Base scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :opened, -> { with_state(:opened) } scope :merged, -> { with_state(:merged) } scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -240,7 +241,7 @@ class MergeRequest < ActiveRecord::Base return unless unchecked? can_be_merged = - project.repository.can_be_merged?(source_sha, target_branch) + !broken? && project.repository.can_be_merged?(source_sha, target_branch) if can_be_merged mark_as_mergeable diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c9a0ad8b9b667b7ac557e06dad48121481ad2332..9c4476c768e5c3763aff2e02f33da460e3f2db6d 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -34,7 +34,7 @@ class Milestone < ActiveRecord::Base scope :closed, -> { with_state(:closed) } scope :of_projects, ->(ids) { where(project_id: ids) } - validates :title, presence: true + validates :title, presence: true, uniqueness: { scope: :project_id } validates :project, presence: true strip_attributes :title diff --git a/app/models/project.rb b/app/models/project.rb index 043f08b9a13a36f4245f9a8aa0f6daeb934b4614..a43878ebcad31c3de4ea9e678890ef826580dc1a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -342,7 +342,7 @@ class Project < ActiveRecord::Base end def repository - @repository ||= Repository.new(path_with_namespace, nil, self) + @repository ||= Repository.new(path_with_namespace, self) end def commit(id = 'HEAD') @@ -738,11 +738,20 @@ class Project < ActiveRecord::Base def hook_attrs { name: name, - ssh_url: ssh_url_to_repo, - http_url: http_url_to_repo, + description: description, web_url: web_url, + avatar_url: avatar_url, + git_ssh_url: ssh_url_to_repo, + git_http_url: http_url_to_repo, namespace: namespace.name, - visibility_level: visibility_level + visibility_level: visibility_level, + path_with_namespace: path_with_namespace, + default_branch: default_branch, + # Backward compatibility + homepage: web_url, + url: url_to_repo, + ssh_url: ssh_url_to_repo, + http_url: http_url_to_repo } end @@ -790,6 +799,8 @@ class Project < ActiveRecord::Base def change_head(branch) # Cached divergent commit counts are based on repository head repository.expire_branch_cache + repository.expire_root_ref_cache + gitlab_shell.update_repository_head(self.path_with_namespace, branch) reload_default_branch end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3d7e8bbee61a304bf66dddc64569a60c77786fa4..e76d9eca2abadba4bc03d97652440f359628004c 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -112,7 +112,7 @@ class PushoverService < Service priority: priority, title: "#{project.name_with_namespace}", message: message, - url: data[:repository][:homepage], + url: data[:project][:web_url], url_title: "See project #{project.name_with_namespace}" } diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c847eba8d1c9b340fef154c1c4391f7f81f8d7bf..c96e6f0b8ea3f6b954495690620fa520d65e6700 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -123,7 +123,7 @@ class ProjectWiki end def repository - Repository.new(path_with_namespace, default_branch, @project) + Repository.new(path_with_namespace, @project) end def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index e813c946bc1a0a80f5bbaf5540c58f0f9c130ffa..ba275fd9803cc904fbafe737498f9414cae54cf8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -15,7 +15,7 @@ class Repository Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) end - def initialize(path_with_namespace, default_branch = nil, project = nil) + def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project end @@ -44,7 +44,9 @@ class Repository end def empty? - raw_repository.empty? + return @empty unless @empty.nil? + + @empty = cache.fetch(:empty?) { raw_repository.empty? } end # @@ -57,7 +59,11 @@ class Repository # This method return true if repository contains some content visible in project page. # def has_visible_content? - raw_repository.branch_count > 0 + return @has_visible_content unless @has_visible_content.nil? + + @has_visible_content = cache.fetch(:has_visible_content?) do + raw_repository.branch_count > 0 + end end def commit(id = 'HEAD') @@ -78,7 +84,8 @@ class Repository offset: offset, # --follow doesn't play well with --skip. See: # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 - follow: false + follow: false, + skip_merges: skip_merges } commits = Gitlab::Git::Commit.where(options) @@ -199,12 +206,6 @@ class Repository readme version contribution_guide changelog license) end - def branch_cache_keys - branches.map do |branch| - :"diverging_commit_counts_#{branch.name}" - end - end - def build_cache cache_keys.each do |key| unless cache.exist?(key) @@ -229,20 +230,39 @@ class Repository @branches = nil end - def expire_cache + def expire_cache(branch_name = nil) cache_keys.each do |key| cache.expire(key) end - expire_branch_cache + expire_branch_cache(branch_name) end - def expire_branch_cache - branches.each do |branch| - cache.expire(:"diverging_commit_counts_#{branch.name}") + def expire_branch_cache(branch_name = nil) + # When we push to the root branch we have to flush the cache for all other + # branches as their statistics are based on the commits relative to the + # root branch. + if !branch_name || branch_name == root_ref + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + end + # In case a commit is pushed to a non-root branch we only have to flush the + # cache for said branch. + else + cache.expire(:"diverging_commit_counts_#{branch_name}") end end + def expire_root_ref_cache + cache.expire(:root_ref) + @root_ref = nil + end + + def expire_has_visible_content_cache + cache.expire(:has_visible_content?) + @has_visible_content = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -480,7 +500,7 @@ class Repository end def root_ref - @root_ref ||= raw_repository.root_ref + @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } end def commit_dir(user, path, message, branch) diff --git a/app/models/user.rb b/app/models/user.rb index a8e602f925a2c1e06b4f4ca3616360bf766ccfd3..1b0c82f45c4a409b597f26a37549645778e650dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -151,16 +151,8 @@ class User < ActiveRecord::Base # Validations # validates :name, presence: true - - [:avatar_crop_x, :avatar_crop_y, :avatar_crop_size].each do |field| - validates field, numericality: { only_integer: true }, allow_blank: true - end - - # Note that a 'uniqueness' and presence check is provided by devise :validatable for email. We do not need to - # duplicate that here as the validation framework will have duplicate errors in the event of a failure. - validates :email, presence: true, email: { strict_mode: true } - validates :notification_email, presence: true, email: { strict_mode: true } - validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true + validates :notification_email, presence: true, email: true + validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :username, @@ -176,6 +168,10 @@ class User < ActiveRecord::Base validate :owns_public_email, if: ->(user) { user.public_email_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + [:avatar_crop_x, :avatar_crop_y, :avatar_crop_size].each do |field| + validates field, numericality: { only_integer: true }, allow_blank: true + end + before_validation :generate_password, on: :create before_validation :restricted_signup_domains, on: :create before_validation :sanitize_attrs diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index f469b13e90277f0be45954ab9f40543e7e5f5ebc..005a5c4661ca0750e0c96c4b67cf253e94592d73 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,28 +1,23 @@ module Ci class ImageForBuildService - def execute(project, params) - sha = params[:sha] - sha ||= - if params[:ref] - project.commit(params[:ref]).try(:sha) - end + def execute(project, opts) + sha = opts[:sha] || ref_sha(project, opts[:ref]) commit = project.ci_commits.ordered.find_by(sha: sha) image_name = image_for_commit(commit) image_path = Rails.root.join('public/ci', image_name) - - OpenStruct.new( - path: image_path, - name: image_name - ) + OpenStruct.new(path: image_path, name: image_name) end private + def ref_sha(project, ref) + project.commit(ref).try(:sha) if ref + end + def image_for_commit(commit) return 'build-unknown.svg' unless commit - 'build-' + commit.status + ".svg" end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index c0e08a151f208f1198bca24772ed9e4d70ef8311..707c2f7ff85ffd87372ffab31b7b99656fecd430 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -29,11 +29,7 @@ class CreateBranchService < BaseService end if new_branch - push_data = build_push_data(project, current_user, new_branch) - - project.execute_hooks(push_data.dup, :push_hooks) - project.execute_services(push_data.dup, :push_hooks) - + # GitPushService handles execution of services and hooks for branch pushes success(new_branch) else error('Invalid reference name') diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 004b3ce7286ed674065e7c595e0c6e3eb2e62473..fae069ee4a52152cae50078c1646e8027767f5bf 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -25,11 +25,7 @@ class DeleteBranchService < BaseService end if repository.rm_branch(current_user, branch_name) - push_data = build_push_data(branch) - - project.execute_hooks(push_data.dup, :push_hooks) - project.execute_services(push_data.dup, :push_hooks) - + # GitPushService handles execution of services and hooks for branch pushes success('Branch was removed') else error('Failed to remove branch') diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index d7ea30bc3157e851a59c7a8b4a9a0b8a032c5976..e3bf14966c84e011ff59ba161a36eef2ea8161ba 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -18,18 +18,23 @@ class GitPushService def execute(project, user, oldrev, newrev, ref) @project, @user = project, user - project.repository.expire_cache + branch_name = Gitlab::Git.ref_name(ref) + + project.repository.expire_cache(branch_name) if push_remove_branch?(ref, newrev) + project.repository.expire_has_visible_content_cache + @push_commits = [] elsif push_to_new_branch?(ref, oldrev) + project.repository.expire_has_visible_content_cache + # Re-find the pushed commits. if is_default_branch?(ref) # Initial push to the default branch. Take the full history of that branch as "newly pushed". @push_commits = project.repository.commits(newrev) # Ensure HEAD points to the default branch in case it is not master - branch_name = Gitlab::Git.ref_name(ref) project.change_head(branch_name) # Set protection on the default branch if configured diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1083bcec05414e3f5664f30c2d58f07362dac75c..edced010811b9b9a7fe5a4579751a0d1755d8a09 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -274,12 +274,15 @@ class SystemNoteService # Check if a cross reference to a noteable from a mentioner already exists # # This method is used to prevent multiple notes being created for a mention - # when a issue is updated, for example. + # when a issue is updated, for example. The method also calls notes_for_mentioner + # to check if the mentioner is a commit, and return matches only on commit hash + # instead of project + commit, to avoid repeated mentions from forks. # # noteable - Noteable object being referenced # mentioner - Mentionable object # # Returns Boolean + def self.cross_reference_exists?(noteable, mentioner) # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) @@ -291,14 +294,20 @@ class SystemNoteService notes = notes.where(noteable_id: noteable.id) end - gfm_reference = mentioner.gfm_reference(noteable.project) - notes = notes.where(note: cross_reference_note_content(gfm_reference)) - - notes.count > 0 + notes_for_mentioner(mentioner, noteable, notes).count > 0 end private + def self.notes_for_mentioner(mentioner, noteable, notes) + if mentioner.is_a?(Commit) + notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}") + else + gfm_reference = mentioner.gfm_reference(noteable.project) + notes.where(note: cross_reference_note_content(gfm_reference)) + end + end + def self.create_note(args = {}) Note.create(args.merge(system: true)) end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb index b35af10080399cd8358170ca2d97c9514196d4d2..aab07a7ece4531264c6460147d1fc3d05bd20383 100644 --- a/app/validators/email_validator.rb +++ b/app/validators/email_validator.rb @@ -1,18 +1,5 @@ -# EmailValidator -# -# Based on https://github.com/balexand/email_validator -# -# Extended to use only strict mode with following allowed characters: -# ' - apostrophe -# -# See http://www.remote.org/jochen/mail/info/chars.html -# class EmailValidator < ActiveModel::EachValidator - PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze - def validate_each(record, attribute, value) - unless value =~ PATTERN - record.errors.add(attribute, options[:message] || :invalid) - end + record.errors.add(attribute, :invalid) unless value =~ Devise.email_regexp end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index b4e3d96d40588d147cb9224a3fecaa0e3563f4cf..b30dfd109ea181c95d7c8269ee2a18694b487f6a 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -47,6 +47,16 @@ = f.label :version_check_enabled do = f.check_box :version_check_enabled Version check enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-group = f.label :admin_notification_email, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 953b8b693684a47a9454ab3c1411b72515f94154..5c9403fa0c2bcaed139b6a05f8e99772510525d9 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -1,6 +1,7 @@ .broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) } = icon('bullhorn') - %span= @broadcast_message.message || "Your message here" + .js-broadcast-message-preview + = render_broadcast_message(@broadcast_message.message.presence || "Your message here") = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f| -if @broadcast_message.errors.any? @@ -10,7 +11,9 @@ .form-group = f.label :message, class: 'control-label' .col-sm-10 - = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true + = f.text_area :message, class: "form-control js-quick-submit js-autosize", + required: true, + data: { preview_path: preview_admin_broadcast_messages_path } .form-group.js-toggle-colors-container .col-sm-10.col-sm-offset-2 = link_to 'Customize colors', '#', class: 'js-toggle-colors-link' diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml new file mode 100644 index 0000000000000000000000000000000000000000..fbc9453c72ee5b5e5bec4fefd71dc4af1d3bf5c3 --- /dev/null +++ b/app/views/admin/broadcast_messages/preview.js.haml @@ -0,0 +1 @@ +$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}"); diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index c395bd908c334a01d8f8ca65c121cb798d4cf044..34d955568f2148230c5851f6194664ee1cc1ed7e 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -4,7 +4,7 @@ = ci_status_with_icon(build.status) %td.build-link - - if build.target_url + - if can?(current_user, :read_build, project) && build.target_url = link_to build.target_url do %strong Build ##{build.id} - else @@ -60,10 +60,10 @@ %td .pull-right - - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts? + - if can?(current_user, :read_build, project) && build.artifacts? = link_to build.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - - if current_user && can?(current_user, :manage_builds, build.project) + - if can?(current_user, :update_build, build.project) - if build.active? - if build.cancel_url = link_to build.cancel_url, method: :post, title: 'Cancel' do diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index d46998ec1e951564d711ec026e4e384fce4504e0..4bc761b3738286f6614a6fed89c4ed87afe92f82 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -14,7 +14,7 @@ .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short', spellcheck: false, id: 'project-filter-form-field' + = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field' = render 'explore/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 66a4b535ae557dbefeac828a89f756c28280b9e7..c248dbb695f5e994ff51238096d5eb186e613369 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,11 +1,3 @@ -.pull-left - = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false - = hidden_field_tag :sort, @sort - .form-group - = button_tag 'Search', class: "btn" - .pull-right.hidden-sm.hidden-xs - if current_user .dropdown.inline.append-right-10 diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index 669079e9521733e02500e87d32c8b81b6c87f766..999a933390b1771eec88dfa0f21508211d72fe9a 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -1,5 +1,5 @@ - if projects.any? - .public-projects + .projects-list-holder = render 'shared/projects/list', projects: projects - else .nothing-here-block diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index a829479bb38283b2ae9bb0a266c6a23c7994d641..9c16ab7e30f74a8c89c2e8dca3ae29a90c523c2b 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,10 +1,11 @@ -.projects-list-holder.prepend-top-default - .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - - if can? current_user, :create_projects, @group - %span.input-group-btn - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new' do - = icon('plus') - New Project +.top-area + .nav-controls + = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| + = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'input-short project-filter-form-field form-control projects-list-filter', spellcheck: false, id: 'project-filter-form-field' + - if current_user && current_user.can_create_project? + = link_to new_project_path, class: 'btn btn-new' do + = icon('plus') + New Project +.projects-list-holder = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index ebb3df7dca3881b9fe7435926f8c44cfa5d43a36..a0ba11b11a14a61d1d816508a81e1a3e9af846a9 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -47,7 +47,7 @@ = render 'shared/event_filter' - .content_list + .content_list{data: {href: events_group_path}} = spinner .tab-pane#projects diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 00cb4aa24cc28b60b4e1c2c98efc7eff2050a1b6..12ded41fbf2ae5727d6fa86405db616700316869 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,5 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @note.author_name, user_url(@note.author)} wrote: %div = markdown(@note.note, pipeline: :email) diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index f4e9749e5c7badec4d08577ded0e4a01a03374e5..81d650373122fb964aa2f1fe7bb6822f9c5a9376 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,9 +1,10 @@ - content_for :header do %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} GitLab (build failed) + %h3 Project: - = link_to ci_project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name %p diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 8b004d34ccaa89aaecf812ccf871590e5e4860a6..5d247eb4cf2e2e2b15be8279ca347b7179de2155 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -4,7 +4,7 @@ %h3 Project: - = link_to ci_project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name %p diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index d3b799fca23840696c50e71d536b4545f73aa13b..ad3ab2525bbfca03382c69a93a601f3b371e3c89 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,3 +1,6 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @issue.author_name, user_url(@issue.author)} wrote: -if @issue.description = markdown(@issue.description, pipeline: :email) diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 90ebdfc3fe278db5e06c928f12fb62e04787ce6e..23423e7d98101981c9bb0f3fd9084917ea3f3e39 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -1,3 +1,6 @@ +- if current_application_settings.email_author_in_body + %div + #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote: %p.details != merge_path_description(@merge_request, '→') diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 630d0286f2509121083629241bd76658acc2d46c..5e3bd14565e7a48609a7252c4798b6ac8d915f7e 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -22,7 +22,7 @@ = number_with_delimiter(@all_builds.finished.count(:id)) .nav-controls - - if can?(current_user, :manage_builds, @project) + - if can?(current_user, :update_build, @project) - if @all_builds.running_or_pending.any? = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index ba1fdc6f0e7416ea88419de2784facb9baf96e73..ca1441a20d8f40de0964bd1915498b2eb22a6a3e 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -89,8 +89,7 @@ Test coverage %h1 #{@build.coverage}% - - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts? - + - if can?(current_user, :read_build, @project) && @build.artifacts? .build-widget.artifacts %h4.title Build artifacts .center @@ -102,7 +101,7 @@ .build-widget %h4.title Build ##{@build.id} - - if current_user && can?(current_user, :manage_builds, @project) + - if can?(current_user, :update_build, @project) .pull-right - if @build.cancel_url = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 329aaa0bb8b77bfd4ee9dbb76aceccd872db2776..befad27666c320067f017b9df5d4e086bc70ce86 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,6 +1,6 @@ .gray-content-block.middle-block .pull-right - - if can?(current_user, :manage_builds, @ci_commit.project) + - if can?(current_user, :update_build, @ci_commit.project) - if @ci_commit.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 2e3c956ddc44b7aa8665cd4dd19ee02a87c6c3c3..a3449d1ae057a42423bc2275f8c0433107885b85 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -1,6 +1,6 @@ %tr.commit_status %td.status - - if commit_status.target_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do = ci_icon_for_status(commit_status.status) = commit_status.status @@ -8,14 +8,14 @@ = ci_status_with_icon(commit_status.status) %td.commit_status-link - - if commit_status.target_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.target_url = link_to commit_status.target_url do %strong ##{commit_status.id} - else %strong ##{commit_status.id} - if commit_status.show_warning? - %i.fa.fa-warning.text-warning + %i.fa.fa-warning.text-warning{data: { toggle: "tooltip" }, title: "This build is stuck, open it to know more"} - if defined?(commit_sha) && commit_sha %td @@ -66,10 +66,10 @@ %td .pull-right - - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts_download_url + - if can?(current_user, :read_commit_status, commit_status) && commit_status.artifacts_download_url = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - - if current_user && can?(current_user, :manage_builds, commit_status.project) + - if can?(current_user, :update_commit_status, commit_status) - if commit_status.active? - if commit_status.cancel_url = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index ede64d47ab339862072a141a6e66d75b16d70183..c52cf25d40ab2abf785d3166618467535d85ee4e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -11,7 +11,10 @@ = render 'shared/ref_switcher', destination: 'commits' .block-controls.hidden-xs.hidden-sm - - if create_mr_button?(@repository.root_ref, @ref) + - if @merge_request.present? + .control + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) .control = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do = icon('plus') diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index efc25eda26be33bef3d3bae4b1f9adbaf054472e..4ab81f3635cc37d19003bd153e486de07bcc17a2 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -13,12 +13,13 @@ = text_field_tag :to, params[:to], class: "form-control", required: true   = button_tag "Compare", class: "btn btn-create commits-compare-btn" - - if create_mr_button? + - if @merge_request.present? + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn' + - elsif create_mr_button? = link_to create_mr_path, class: 'prepend-left-10 btn' do = icon("plus") Create Merge Request - :javascript var availableTags = #{@project.repository.ref_names.to_json}; diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 058b71b21f5173efd9c37bd02377dd691d5e7af4..4fcf7ea0b26283f55fcfc73c225a223d3e4f6e19 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,4 +1,5 @@ - diff = diff_file.diff +- file.load_all_data!(@project.repository) - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap @@ -6,6 +7,7 @@ %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} %p.image-info= "#{number_to_human_size file.size}" - else + - old_file.load_all_data!(@project.repository) .image %div.two-up.view %span.wrap diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8a99aceef7f87f5214d855e18fba6f7774c5f760..fdcb698747146c7cf9c41a67a3a37128dca7b0f7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -130,6 +130,7 @@ %strong git fetch %br %span.descr Faster + .form-group = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' .col-sm-10 @@ -158,6 +159,13 @@ phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public builds + .help-block Allow everyone to access builds for Public and Internal projects %fieldset.features %legend diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index acb2353d3cad298afd9fcab984087dadb869e04a..42fa6fdb782709d43fef3eaf7cba0eeb4d601018 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -6,7 +6,7 @@ == #{pluralize(@all_forks.size, 'fork')}: #{full_count_title} .nav-controls - = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter form-control input-short', + = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short', spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } .dropdown @@ -29,14 +29,15 @@ = link_to page_filter_path(sort: sort_value_oldest_updated, without: excluded_filters) do = sort_title_oldest_updated - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do - = icon('code-fork fw') - Fork - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do - = icon('code-fork fw') - Fork + - if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do + = icon('code-fork fw') + Fork + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do + = icon('code-fork fw') + Fork .projects-list-holder diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 121c775560f22676476983c8e6d17c46ae43ab0b..fe977fd700cbe14ff5251f1c74c3805b6b84bd4e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -46,8 +46,8 @@ Edited = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') - .merge-requests - = render 'merge_requests' + .merge-requests + = render 'merge_requests' .content-block = render 'votes/votes_block', votable: @issue @@ -57,4 +57,4 @@ .issuable-discussion = render 'projects/issues/discussion' -= render 'shared/issuable/sidebar', issuable: @issue \ No newline at end of file += render 'shared/issuable/sidebar', issuable: @issue diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 3c5edf4b033f1a472953fa081343e70486175b1c..baaa2caa6de827bee5791c25210feb364e31533e 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,7 +1,7 @@ %article.file-holder.readme-holder .file-title = blob_icon readme.mode, readme.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, @path, readme.name)) do %strong = readme.name .file-content.wiki diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index e80dffc1ced3360aa26e8b58e08d9916fe3f4697..efe1e6f24c22e144d7b32de64321dfbd9d1fe1c3 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -3,9 +3,11 @@ Secret Variables %p.light - These variables will be set to environment by the runner and will be hidden in the build log. + These variables will be set to environment by the runner. %br So you can use them for passwords, secret keys or whatever you want. + %br + The value of the variable can be visible in build log if explicitly asked to do so. %hr diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index dcd6119971736c27d651482c2f218b999c2af8c2..6b77d24f50c2f476b26d6aa0c96fa2d600b37eed 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -1,46 +1,50 @@ +- snippet_blob = chunk_snippet(snippet_blob, @search_term) +- snippet = snippet_blob[:snippet_object] +- snippet_chunks = snippet_blob[:snippet_chunks] + .search-result-row %span - = snippet_blob[:snippet_object].title + = snippet.title by - = link_to user_snippets_path(snippet_blob[:snippet_object].author) do - = image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: '' - = snippet_blob[:snippet_object].author_name - %span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)} + = link_to user_snippets_path(snippet.author) do + = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' + = snippet.author_name + %span.light #{time_ago_with_tooltip(snippet.created_at)} %h4.snippet-title - - snippet_path = reliable_snippet_path(snippet_blob[:snippet_object]) + - snippet_path = reliable_snippet_path(snippet) = link_to snippet_path do .file-holder .file-title %i.fa.fa-file - %strong= snippet_blob[:snippet_object].file_name - - if markup?(snippet_blob[:snippet_object].file_name) + %strong= snippet.file_name + - if markup?(snippet.file_name) .file-content.wiki - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = render_markup(snippet_blob[:snippet_object].file_name, snippet[:data]) + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = render_markup(snippet.file_name, chunk[:data]) - else .file-content.code .nothing-here-block Empty file - else .file-content.code.js-syntax-highlight .line-numbers - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - - snippet[:data].lines.to_a.size.times do |index| - - offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1 + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + - chunk[:data].lines.to_a.size.times do |index| + - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1 - i = index + offset = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do %i.fa.fa-link = i - - unless snippet == snippet_blob[:snippet_chunks].last + - unless snippet == snippet_chunks.last %a.diff-line-num = "." %pre.code %code - - snippet_blob[:snippet_chunks].each do |snippet| - - unless snippet[:data].empty? - = snippet[:data] - - unless snippet == snippet_blob[:snippet_chunks].last + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = chunk[:data] + - unless chunk == snippet_chunks.last %a = "..." - else diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 285af56ad73c28bcf4a30a80c974669377138df7..627814bcfae11e1bf33e59bd0de20090a3f1e1d5 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -11,6 +11,6 @@ %li If your HTTP repository is not publicly accessible, add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. + The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. %li To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 3d279ec228cccba9d8b0bb85e2659a003295b3c7..b07f1c5603e6cbf7b3fe53f6e64055140f11ddbc 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,21 +1,9 @@ -ASCII' - - allow(Asciidoctor).to receive(:convert).and_return(html) - expect(Banzai).to receive(:render) - .with(html, context.merge(pipeline: :asciidoc)) - .and_return(filtered_html) - - expect( render('foo', context) ).to eql filtered_html - end - end - def render(*args) described_class.render(*args) end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index bd8688fefa1da0c1cdffb6c116244630b6f1fe83..d0a447753b719790371268c3a02841335cf4fbbd 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,5 +1,9 @@ require 'spec_helper' +class MigrationTest + include Gitlab::Database +end + describe Gitlab::Database, lib: true do # These are just simple smoke tests to check if the methods work (regardless # of what they may return). @@ -34,4 +38,32 @@ describe Gitlab::Database, lib: true do end end end + + describe '#true_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.true_value).to eq "'t'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.true_value).to eq 1 + end + end + + describe '#false_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.false_value).to eq "'f'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.false_value).to eq 0 + end + end end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index 691f36e6cb74eab260512ba516fe7077a36a1a24..da6526774437aa24bc4551ffce44db080520ab13 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -16,62 +16,80 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on commit' do - let(:note) { create(:note_on_commit) } + let(:note) { create(:note_on_commit, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff) } + let(:note) { create(:note_on_commit_diff, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on issue' do let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_issue, noteable_id: issue.id) } + let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) } it 'returns the note and issue-specific data' do expect(data).to have_key(:issue) expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at')) expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request diff' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on project snippet' do let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } - let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) } it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 3ef61685398601a3f09c160fa2ac607631bb8e59..257e4a38435cdc17a456634d08f64c426cb9a797 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -13,13 +13,13 @@ describe 'Gitlab::PushDataBuilder', lib: true do it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { expect(data[:ref]).to eq('refs/heads/master') } it { expect(data[:commits].size).to eq(3) } - it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) } - it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } - it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } it { expect(data[:commits].first[:removed]).to eq([]) } + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe :build do diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0df89938e971df1e198d645ae15dd1e72e04a86d --- /dev/null +++ b/spec/mailers/emails/builds_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + + include_context 'gitlab email notification' + + describe 'build notification email' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + shared_examples 'build email' do + it 'contains name of project' do + is_expected.to have_body_text build.project_name + end + + it 'contains link to project' do + is_expected.to have_body_text namespace_project_path(project.namespace, project) + end + end + + shared_examples 'an email with X-GitLab headers containing build details' do + it 'has X-GitLab-Build* headers' do + is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ + is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ + end + end + + describe 'build success' do + subject { Notify.build_success_email(build.id, 'wow@example.com') } + before { build.success } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'success' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build success for/ + end + end + + describe 'build fail' do + subject { Notify.build_fail_email(build.id, 'wow@example.com') } + before { build.drop } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'failed' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build failed for/ + end + end + end +end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5b575da34f35529481624aab1c8808f7cbc1670a --- /dev/null +++ b/spec/mailers/emails/profile_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + include_context 'gitlab email notification' + + describe 'profile notifications' do + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + let(:token) { 'kETLwRaayvigPq_x3SNM' } + + subject { Notify.new_user_email(new_user.id, token) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'contains the password text' do + is_expected.to have_body_text /Click here to set your password/ + end + + it 'includes a link for user to set password' do + params = "reset_password_token=#{token}" + is_expected.to have_body_text( + %r{http://localhost(:\d+)?/users/password/edit\?#{params}} + ) + end + + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) + end + end + + describe 'for users that signed up, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } + + subject { Notify.new_user_email(new_user.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'should not contain the new user\'s password' do + is_expected.not_to have_body_text /password/ + end + end + + describe 'user added ssh key' do + let(:key) { create(:personal_key) } + + subject { Notify.new_ssh_key_email(key.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to key.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^SSH key was added to your account$/i + end + + it 'contains the new ssh key title' do + is_expected.to have_body_text /#{key.title}/ + end + + it 'includes a link to ssh keys page' do + is_expected.to have_body_text /#{profile_keys_path}/ + end + end + + describe 'user added email' do + let(:email) { create(:email) } + + subject { Notify.new_email_email(email.id) } + + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to email.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^Email was added to your account$/i + end + + it 'contains the new email address' do + is_expected.to have_body_text /#{email.email}/ + end + + it 'includes a link to emails page' do + is_expected.to have_body_text /#{profile_emails_path}/ + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 7289e596ef3da67ef306fbe466af2c8ac76a5a1d..232a11245a6116d9644f3d2b3b8e45bc54d8a66b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1,237 +1,13 @@ require 'spec_helper' require 'email_spec' +require 'mailers/shared/notify' describe Notify do include EmailSpec::Helpers include EmailSpec::Matchers include RepoHelpers - new_user_address = 'newguy@example.com' - - let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } - let(:gitlab_sender) { Gitlab.config.gitlab.email_from } - let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } - let(:recipient) { create(:user, email: 'recipient@example.com') } - let(:project) { create(:project) } - let(:build) { create(:ci_build) } - - before(:each) do - ActionMailer::Base.deliveries.clear - email = recipient.emails.create(email: "notifications@example.com") - recipient.update_attribute(:notification_email, email.email) - end - - shared_examples 'a multiple recipients email' do - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email - end - end - - shared_examples 'an email sent from GitLab' do - it 'is sent from GitLab' do - sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(gitlab_sender_display_name) - expect(sender.address).to eq(gitlab_sender) - end - - it 'has a Reply-To address' do - reply_to = subject.header[:reply_to].addresses - expect(reply_to).to eq([gitlab_sender_reply_to]) - end - end - - shared_examples 'an email with X-GitLab headers containing project details' do - it 'has X-GitLab-Project* headers' do - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ - is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ - end - end - - shared_examples 'an email with X-GitLab headers containing build details' do - it 'has X-GitLab-Build* headers' do - is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ - is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ - end - end - - shared_examples 'an email that contains a header with author username' do - it 'has X-GitLab-Author header containing author\'s username' do - is_expected.to have_header 'X-GitLab-Author', user.username - end - end - - shared_examples 'an email starting a new thread' do |message_id_prefix| - include_examples 'an email with X-GitLab headers containing project details' - - it 'has a discussion identifier' do - is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - end - end - - shared_examples 'an answer to an existing thread' do |thread_id_prefix| - include_examples 'an email with X-GitLab headers containing project details' - - it 'has a subject that begins with Re: ' do - is_expected.to have_subject /^Re: / - end - - it 'has headers that reference an existing thread' do - is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - end - end - - shared_examples 'a new user email' do |user_email, site_path| - it 'is sent to the new user' do - is_expected.to deliver_to user_email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{user_email}/ - end - - it 'includes a link to the site' do - is_expected.to have_body_text /#{site_path}/ - end - end - - shared_examples 'it should have Gmail Actions links' do - it { is_expected.to have_body_text /ViewAction/ } - end - - shared_examples 'it should not have Gmail Actions links' do - it { is_expected.to_not have_body_text /ViewAction/ } - end - - shared_examples 'it should show Gmail Actions View Issue link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Issue/ } - end - - shared_examples 'it should show Gmail Actions View Merge request link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Merge request/ } - end - - shared_examples 'it should show Gmail Actions View Commit link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Commit/ } - end - - shared_examples 'an unsubscribeable thread' do - it { is_expected.to have_body_text /unsubscribe/ } - end - - shared_examples "a user cannot unsubscribe through footer link" do - it { is_expected.not_to have_body_text /unsubscribe/ } - end - - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } - - token = 'kETLwRaayvigPq_x3SNM' - - subject { Notify.new_user_email(new_user.id, token) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'contains the password text' do - is_expected.to have_body_text /Click here to set your password/ - end - - it 'includes a link for user to set password' do - params = "reset_password_token=#{token}" - is_expected.to have_body_text( - %r{http://localhost(:\d+)?/users/password/edit\?#{params}} - ) - end - - it 'explains the reset link expiration' do - is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) - is_expected.to have_body_text(new_user_password_url) - is_expected.to have_body_text(/\?user_email=.*%40.*/) - end - end - - describe 'for users that signed up, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } - - subject { Notify.new_user_email(new_user.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'should not contain the new user\'s password' do - is_expected.not_to have_body_text /password/ - end - end - - describe 'user added ssh key' do - let(:key) { create(:personal_key) } - - subject { Notify.new_ssh_key_email(key.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'is sent to the new user' do - is_expected.to deliver_to key.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^SSH key was added to your account$/i - end - - it 'contains the new ssh key title' do - is_expected.to have_body_text /#{key.title}/ - end - - it 'includes a link to ssh keys page' do - is_expected.to have_body_text /#{profile_keys_path}/ - end - end - - describe 'user added email' do - let(:email) { create(:email) } - - subject { Notify.new_email_email(email.id) } - - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like 'a user cannot unsubscribe through footer link' - - it 'is sent to the new user' do - is_expected.to deliver_to email.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Email was added to your account$/i - end - - it 'contains the new email address' do - is_expected.to have_body_text /#{email.email}/ - end - - it 'includes a link to emails page' do - is_expected.to have_body_text /#{profile_emails_path}/ - end - end + include_context 'gitlab email notification' context 'for a project' do describe 'items that are assignable, the email' do @@ -270,6 +46,17 @@ describe Notify do it 'contains a link to the new issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text issue.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do @@ -377,6 +164,17 @@ describe Notify do it 'has the correct message-id set' do is_expected.to have_header 'Message-ID', "" end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text merge_request.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do @@ -550,6 +348,21 @@ describe Notify do it 'contains the message from the note' do is_expected.to have_body_text /#{note.note}/ end + + it 'not contains note author' do + is_expected.not_to have_body_text /wrote\:/ + end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text note.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'on a commit' do @@ -934,49 +747,4 @@ describe Notify do end end - describe 'build success' do - before { build.success } - - subject { Notify.build_success_email(build.id, 'wow@example.com') } - - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' do - let(:project) { build.project } - end - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'success' - end - - it 'has the correct subject' do - should have_subject /Build success for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end - - describe 'build fail' do - before { build.drop } - - subject { Notify.build_fail_email(build.id, 'wow@example.com') } - - it_behaves_like 'an email with X-GitLab headers containing build details' - it_behaves_like 'an email with X-GitLab headers containing project details' do - let(:project) { build.project } - end - - it 'has header indicating build status' do - is_expected.to have_header 'X-GitLab-Build-Status', 'failed' - end - - it 'has the correct subject' do - should have_subject /Build failed for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb new file mode 100644 index 0000000000000000000000000000000000000000..48c851ebbd6af1d9a194b035555015cf06665475 --- /dev/null +++ b/spec/mailers/shared/notify.rb @@ -0,0 +1,117 @@ +shared_context 'gitlab email notification' do + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } + let(:gitlab_sender) { Gitlab.config.gitlab.email_from } + let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } + let(:recipient) { create(:user, email: 'recipient@example.com') } + let(:project) { create(:project) } + let(:new_user_address) { 'newguy@example.com' } + + before do + ActionMailer::Base.deliveries.clear + email = recipient.emails.create(email: "notifications@example.com") + recipient.update_attribute(:notification_email, email.email) + end +end + +shared_examples 'a multiple recipients email' do + it 'is sent to the given recipient' do + is_expected.to deliver_to recipient.notification_email + end +end + +shared_examples 'an email sent from GitLab' do + it 'is sent from GitLab' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has a Reply-To address' do + reply_to = subject.header[:reply_to].addresses + expect(reply_to).to eq([gitlab_sender_reply_to]) + end +end + +shared_examples 'an email that contains a header with author username' do + it 'has X-GitLab-Author header containing author\'s username' do + is_expected.to have_header 'X-GitLab-Author', user.username + end +end + +shared_examples 'an email with X-GitLab headers containing project details' do + it 'has X-GitLab-Project* headers' do + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ + is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ + is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ + end +end + +shared_examples 'an email starting a new thread' do |message_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a discussion identifier' do + is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'an answer to an existing thread' do |thread_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a subject that begins with Re: ' do + is_expected.to have_subject /^Re: / + end + + it 'has headers that reference an existing thread' do + is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'a new user email' do + it 'is sent to the new user' do + is_expected.to deliver_to new_user_address + end + + it 'has the correct subject' do + is_expected.to have_subject /^Account was created for you$/i + end + + it 'contains the new user\'s login name' do + is_expected.to have_body_text /#{new_user_address}/ + end +end + +shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text /ViewAction/ } +end + +shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text /ViewAction/ } +end + +shared_examples 'it should show Gmail Actions View Issue link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Issue/ } +end + +shared_examples 'it should show Gmail Actions View Merge request link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Merge request/ } +end + +shared_examples 'it should show Gmail Actions View Commit link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Commit/ } +end + +shared_examples 'an unsubscribeable thread' do + it { is_expected.to have_body_text /unsubscribe/ } +end + +shared_examples "a user cannot unsubscribe through footer link" do + it { is_expected.not_to have_body_text /unsubscribe/ } +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f4c588827575ef3f69b4cd9b1b68688d011ae6ac..b1764d7ac091fe45bae9bd4e491971994e442b5b 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -66,6 +66,18 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(http).for(:after_sign_out_path) } it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + + it { is_expected.to validate_presence_of(:max_attachment_size) } + + it do + is_expected.to validate_numericality_of(:max_attachment_size) + .only_integer + .is_greater_than(0) + end + + it_behaves_like 'an object with email-formated attributes', :admin_notification_email do + subject { setting } + end end context 'restricted signup domains' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 021d62cdf0ce324711e69333ece64ed84f2484c8..600089802b2a4bed2e4eb4e8c49551845b2dfa93 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -69,17 +69,28 @@ describe Issue, "Issuable" do end describe "#to_hook_data" do - let(:hook_data) { issue.to_hook_data(user) } + let(:data) { issue.to_hook_data(user) } + let(:project) { issue.project } + it "returns correct hook data" do - expect(hook_data[:object_kind]).to eq("issue") - expect(hook_data[:user]).to eq(user.hook_attrs) - expect(hook_data[:repository][:name]).to eq(issue.project.name) - expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo) - expect(hook_data[:repository][:description]).to eq(issue.project.description) - expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url) - expect(hook_data[:object_attributes]).to eq(issue.hook_attrs) + expect(data[:object_kind]).to eq("issue") + expect(data[:user]).to eq(user.hook_attrs) + expect(data[:object_attributes]).to eq(issue.hook_attrs) + expect(data).to_not have_key(:assignee) end + + context "issue is assigned" do + before { issue.update_attribute(:assignee, user) } + + it "returns correct hook data" do + expect(data[:object_attributes]['assignee_id']).to eq(user.id) + expect(data[:assignee]).to eq(user.hook_attrs) + end + end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe '#card_attributes' do diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a20a614964924301de1fba76b636eca36c21242b --- /dev/null +++ b/spec/models/email_spec.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: emails +# +# id :integer not null, primary key +# user_id :integer not null +# email :string(255) not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Email, models: true do + + describe 'validations' do + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:email) } + end + end + +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2aedca20df257c96ce6b57a523a447b6f53fd5c1..2d8f1cc1ad3362e68ef0e9997f7b081374870f53 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -31,6 +31,10 @@ describe Member, models: true do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + it_behaves_like 'an object with email-formated attributes', :invite_email do + subject { build(:project_member) } + end + context "when an invite email is provided" do let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } @@ -159,7 +163,7 @@ describe Member, models: true do describe "#generate_invite_token" do let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } - + it "sets the invite token" do expect { member.generate_invite_token }.to change { member.invite_token} end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c61ddf01118f5b1fca0854b7fdc03219d31c9e3b..f35b48601ada4856029b9e186053f0ad2faabad5 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -254,13 +254,22 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do + let(:attrs_hash) { subject.hook_attrs.to_h } + + [:source, :target].each do |key| + describe "#{key} key" do + include_examples 'project hook data', project_key: key do + let(:data) { attrs_hash } + let(:project) { subject.send("#{key}_project") } + end + end + end + it "has all the required keys" do - attrs = subject.hook_attrs - attrs = attrs.to_h - expect(attrs).to include(:source) - expect(attrs).to include(:target) - expect(attrs).to include(:last_commit) - expect(attrs).to include(:work_in_progress) + expect(attrs_hash).to include(:source) + expect(attrs_hash).to include(:target) + expect(attrs_hash).to include(:last_commit) + expect(attrs_hash).to include(:work_in_progress) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 30a71987d86d6fa572769ce88a40b153a53c31cc..1b1380ce4e2c3e31996ad00d678837a633531de5 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -33,6 +33,20 @@ describe Milestone, models: true do let(:milestone) { create(:milestone) } let(:issue) { create(:issue) } + describe "unique milestone title per project" do + it "shouldn't accept the same title in a project twice" do + new_milestone = Milestone.new(project: milestone.project, title: milestone.title) + expect(new_milestone).not_to be_valid + end + + it "should accept the same title in another project" do + project = build(:project) + new_milestone = Milestone.new(project: project, title: milestone.title) + + expect(new_milestone).to be_valid + end + end + describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c484ae8fc8cebcd1a445c91931396e343338e124..e1ee43e64db1d0ed9cb3bc31d282acecd7fea414 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -232,11 +232,132 @@ describe Repository, models: true do end describe 'when there are branches' do - before do - allow(repository.raw_repository).to receive(:branch_count).and_return(3) + it 'returns true' do + expect(repository.raw_repository).to receive(:branch_count).and_return(3) + + expect(subject).to eq(true) end - it { is_expected.to eq(true) } + it 'caches the output' do + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(3) + + repository.has_visible_content? + repository.has_visible_content? + end + end + end + + describe '#empty?' do + let(:empty_repository) { create(:project_empty_repo).repository } + + it 'returns true for an empty repository' do + expect(empty_repository.empty?).to eq(true) + end + + it 'returns false for a non-empty repository' do + expect(repository.empty?).to eq(false) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:empty?). + once. + and_return(false) + + repository.empty? + repository.empty? + end + end + + describe '#root_ref' do + it 'returns a branch name' do + expect(repository.root_ref).to be_an_instance_of(String) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('master') + + repository.root_ref + repository.root_ref + end + end + + describe '#expire_cache' do + it 'expires all caches' do + expect(repository).to receive(:expire_branch_cache) + + repository.expire_cache + end + + it 'expires the caches for a specific branch' do + expect(repository).to receive(:expire_branch_cache).with('master') + + repository.expire_cache('master') end end + + describe '#expire_root_ref_cache' do + it 'expires the root reference cache' do + repository.root_ref + + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('foo') + + repository.expire_root_ref_cache + + expect(repository.root_ref).to eq('foo') + end + end + + describe '#expire_has_visible_content_cache' do + it 'expires the visible content cache' do + repository.has_visible_content? + + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(0) + + repository.expire_has_visible_content_cache + + expect(repository.has_visible_content?).to eq(false) + end + end + + describe '#expire_branch_ache' do + # This method is private but we need it for testing purposes. Sadly there's + # no other proper way of testing caching operations. + let(:cache) { repository.send(:cache) } + + it 'expires the cache for all branches' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache + end + + it 'expires the cache for all branches when the root branch is given' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache(repository.root_ref) + end + + it 'expires the cache for a specific branch' do + expect(cache).to receive(:expire).once + + repository.expire_branch_cache('foo') + end + end + + describe :skip_merged_commit do + subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", nil, 100, 0, true).map{ |k| k.id } } + + it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index cee051f5732e104cad76815e7b726bfa628f8fbb..47ce409fe4b9dec381a71ae7a44c55547125f2cc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -119,37 +119,15 @@ describe User, models: true do it { is_expected.to validate_length_of(:bio).is_within(0..255) } - describe 'email' do - it 'accepts info@example.com' do - user = build(:user, email: 'info@example.com') - expect(user).to be_valid - end - - it 'accepts info+test@example.com' do - user = build(:user, email: 'info+test@example.com') - expect(user).to be_valid - end - - it "accepts o'reilly@example.com" do - user = build(:user, email: "o'reilly@example.com") - expect(user).to be_valid - end - - it 'rejects test@test@example.com' do - user = build(:user, email: 'test@test@example.com') - expect(user).to be_invalid - end - - it 'rejects mailto:test@example.com' do - user = build(:user, email: 'mailto:test@example.com') - expect(user).to be_invalid - end + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:user) } + end - it "rejects lol!'+=?><#$%^&*()@gmail.com" do - user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com") - expect(user).to be_invalid - end + it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do + subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } } + end + describe 'email' do context 'when no signup domains listed' do before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) } it 'accepts any email' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 8c9f5a382b7004db977e2b8edfa33e1c10c92a49..6c07802db8b32d041af9871ab2c5f7d298f54738 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -113,7 +113,7 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/cancel' do context 'authorized user' do - context 'user with :manage_builds persmission' do + context 'user with :update_build persmission' do it 'should cancel running or pending build' do post api("/projects/#{project.id}/builds/#{build.id}/cancel", user) @@ -122,7 +122,7 @@ describe API::API, api: true do end end - context 'user without :manage_builds permission' do + context 'user without :update_build permission' do it 'should not cancel build' do post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2) @@ -142,7 +142,7 @@ describe API::API, api: true do describe 'POST /projects/:id/builds/:build_id/retry' do context 'authorized user' do - context 'user with :manage_builds persmission' do + context 'user with :update_build persmission' do it 'should retry non-running build' do post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user) @@ -152,7 +152,7 @@ describe API::API, api: true do end end - context 'user without :manage_builds permission' do + context 'user without :update_build permission' do it 'should not retry build' do post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2) diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index 21482fc1070b40123e0baef34703349d3d740e29..89b554622ef6481ba373ad5a16eb33ff6d682cbc 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -2,18 +2,17 @@ require 'spec_helper' describe API::CommitStatus, api: true do include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, creator_id: user.id) } - let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:project) { create(:project) } let(:commit) { project.repository.commit } let!(:ci_commit) { project.ensure_ci_commit(commit.id) } let(:commit_status) { create(:commit_status, commit: ci_commit) } + let(:guest) { create_user(ProjectMember::GUEST) } + let(:reporter) { create_user(ProjectMember::REPORTER) } + let(:developer) { create_user(ProjectMember::DEVELOPER) } describe "GET /projects/:id/repository/commits/:sha/statuses" do it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) } + let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) } end context "reporter user" do @@ -29,7 +28,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -39,7 +38,7 @@ describe API::CommitStatus, api: true do end it "should return all commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -47,7 +46,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses for specific ref" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -55,7 +54,7 @@ describe API::CommitStatus, api: true do end it "should return latest commit statuses for specific name" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", reporter) expect(response.status).to eq(200) expect(json_response).to be_an Array @@ -65,7 +64,7 @@ describe API::CommitStatus, api: true do context "guest user" do it "should not return project commits" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2) + get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", guest) expect(response.status).to eq(403) end end @@ -81,10 +80,10 @@ describe API::CommitStatus, api: true do describe 'POST /projects/:id/statuses/:sha' do let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } - context 'reporter user' do + context 'developer user' do context 'should create commit status' do it 'with only required parameters' do - post api(post_url, user), state: 'success' + post api(post_url, developer), state: 'success' expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -95,7 +94,7 @@ describe API::CommitStatus, api: true do end it 'with all optional parameters' do - post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' + post api(post_url, developer), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -108,25 +107,32 @@ describe API::CommitStatus, api: true do context 'should not create commit status' do it 'with invalid state' do - post api(post_url, user), state: 'invalid' + post api(post_url, developer), state: 'invalid' expect(response.status).to eq(400) end it 'without state' do - post api(post_url, user) + post api(post_url, developer) expect(response.status).to eq(400) end it 'invalid commit' do - post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running' + post api("/projects/#{project.id}/statuses/invalid_sha", developer), state: 'running' expect(response.status).to eq(404) end end end + context 'reporter user' do + it 'should not create commit status' do + post api(post_url, reporter) + expect(response.status).to eq(403) + end + end + context 'guest user' do it 'should not create commit status' do - post api(post_url, user2) + post api(post_url, guest) expect(response.status).to eq(403) end end @@ -138,4 +144,10 @@ describe API::CommitStatus, api: true do end end end + + def create_user(access_level) + user = create(:user) + create(:project_member, user: user, project: project, access_level: access_level) + user + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index d7bfa17b0b1c7e428df55d071ddc330b468b240d..4fd1df25568a90396fae7fb503a9cae8b0f4acd0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -10,6 +10,7 @@ describe API::API, api: true do let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } before do project.team << [user, :reporters] @@ -115,6 +116,7 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['merge_status']).to eq('can_be_merged') end it 'should return merge_request by iid' do @@ -169,10 +171,12 @@ describe API::API, api: true do source_branch: 'feature_conflict', target_branch: 'master', author: user, - labels: 'label, label2' + labels: 'label, label2', + milestone_id: milestone.id expect(response.status).to eq(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['milestone']['id']).to eq(milestone.id) end it "should return 422 when source_branch equals target_branch" do @@ -373,18 +377,24 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id" do - it "should return merge_request" do + it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response.status).to eq(200) expect(json_response['title']).to eq('New title') end - it "should return merge_request" do + it "updates description and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" expect(response.status).to eq(200) expect(json_response['description']).to eq('New description') end + it "updates milestone_id and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + expect(response.status).to eq(200) + expect(json_response['milestone']['id']).to eq(milestone.id) + end + it "should return 400 when source_branch is specified" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), source_branch: "master", target_branch: "master" @@ -448,6 +458,28 @@ describe API::API, api: true do end end + describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + it 'returns the issue that will be closed on merge' do + issue = create(:issue, project: project) + mr = merge_request.tap do |mr| + mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") + end + + get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array when there are no issues to be closed' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 244947762dd3f136ed7650a27835da1aeec4c0eb..01b369720ca26117b1e64e4d750294656b55efab 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -151,8 +151,8 @@ describe Ci::API::API do context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } - let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } @@ -160,12 +160,10 @@ describe Ci::API::API do let(:headers) { { "GitLab-Workhorse" => "1.0" } } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } + before { build.run! } + describe "POST /builds/:id/artifacts/authorize" do context "should authorize posting artifact to running build" do - before do - build.run! - end - it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response.status).to eq(200) @@ -180,10 +178,6 @@ describe Ci::API::API do end context "should fail to post too large artifact" do - before do - build.run! - end - it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) post authorize_url, { token: build.token, filesize: 100 }, headers @@ -197,8 +191,8 @@ describe Ci::API::API do end end - context "should get denied" do - it do + context 'token is invalid' do + it 'should respond with forbidden'do post authorize_url, { token: 'invalid', filesize: 100 } expect(response.status).to eq(403) end @@ -206,17 +200,13 @@ describe Ci::API::API do end describe "POST /builds/:id/artifacts" do - context "Disable sanitizer" do + context "disable sanitizer" do before do # by configuring this path we allow to pass temp file from any path allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') end context "should post artifact to running build" do - before do - build.run! - end - it "uses regual file post" do upload_artifacts(file_upload, headers_with_token, false) expect(response.status).to eq(201) @@ -244,10 +234,7 @@ describe Ci::API::API do let(:stored_artifacts_file) { build.reload.artifacts_file.file } let(:stored_metadata_file) { build.reload.artifacts_metadata.file } - before do - build.run! - post(post_url, post_data, headers_with_token) - end + before { post(post_url, post_data, headers_with_token) } context 'post data accelerated by workhorse is correct' do let(:post_data) do @@ -257,11 +244,8 @@ describe Ci::API::API do 'metadata.name' => metadata.original_filename } end - it 'responds with valid status' do - expect(response.status).to eq(201) - end - it 'stores artifacts and artifacts metadata' do + expect(response.status).to eq(201) expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) end @@ -282,56 +266,42 @@ describe Ci::API::API do end end - - context "should fail to post too large artifact" do - before do - build.run! - end - - it do + context "artifacts file is too large" do + it "should fail to post too large artifact" do stub_application_setting(max_artifacts_size: 0) upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(413) end end - context "should fail to post artifacts without file" do - before do - build.run! - end - - it do + context "artifacts post request does not contain file" do + it "should fail to post artifacts without file" do post post_url, {}, headers_with_token expect(response.status).to eq(400) end end - context "should fail to post artifacts without GitLab-Workhorse" do - before do - build.run! - end - - it do + context 'GitLab Workhorse is not configured' do + it "should fail to post artifacts without GitLab-Workhorse" do post post_url, { token: build.token }, {} expect(response.status).to eq(403) end end end - context "should fail to post artifacts for outside of tmp path" do + context "artifacts are being stored outside of tmp path" do before do # by configuring this path we allow to pass file from @tmpdir only # but all temporary files are stored in system tmp directory @tmpdir = Dir.mktmpdir allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) - build.run! end after do FileUtils.remove_entry @tmpdir end - it do + it "should fail to post artifacts for outside of tmp path" do upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(400) end @@ -349,33 +319,37 @@ describe Ci::API::API do end end - describe "DELETE /builds/:id/artifacts" do - before do - build.run! - post delete_url, token: build.token, file: file_upload - end + describe 'DELETE /builds/:id/artifacts' do + let(:build) { create(:ci_build, :artifacts) } + before { delete delete_url, token: build.token } - it "should delete artifact build" do - build.success - delete delete_url, token: build.token + it 'should remove build artifacts' do expect(response.status).to eq(200) + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy end end - describe "GET /builds/:id/artifacts" do - before do - build.run! - end + describe 'GET /builds/:id/artifacts' do + before { get get_url, token: build.token } - it "should download artifact" do - build.update_attributes(artifacts_file: file_upload) - get get_url, token: build.token - expect(response.status).to eq(200) + context 'build has artifacts' do + let(:build) { create(:ci_build, :artifacts) } + let(:download_headers) do + { 'Content-Transfer-Encoding'=>'binary', + 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + end + + it 'should download artifact' do + expect(response.status).to eq(200) + expect(response.headers).to include download_headers + end end - it "should fail to download if no artifact uploaded" do - get get_url, token: build.token - expect(response.status).to eq(404) + context 'build does not has artifacts' do + it 'should respond with not found' do + expect(response.status).to eq(404) + end end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index c1080ef190aa2303641d110aa0d45a99b8fa805d..eb3a5fe43f5ff14d7c6de41c13e095d07affca5b 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -21,6 +21,18 @@ describe GitPushService, services: true do end it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end end context 'existing branch' do @@ -29,6 +41,12 @@ describe GitPushService, services: true do end it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end end context 'rm branch' do @@ -37,6 +55,18 @@ describe GitPushService, services: true do end it { is_expected.to be_truthy } + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache).with('master') + + subject + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index d3364a71022c9d953c59971ad4570dfeea2066b3..1bdc03af12d7baa18f6891ecc6e56b7a58be6b9b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -424,6 +424,21 @@ describe SystemNoteService, services: true do to be_falsey end end + + context 'commit with cross-reference from fork' do + let(:author2) { create(:user) } + let(:forked_project) { Projects::ForkService.new(project, author2).execute } + let(:commit2) { forked_project.commit } + + before do + described_class.cross_reference(noteable, commit0, author2) + end + + it 'is true when a fork mentions an external issue' do + expect(described_class.cross_reference_exists?(noteable, commit2)). + to be true + end + end end include JiraServiceHelper diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/email_format_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..b924a208e711c94ce4539329b66758a89bbdfe7e --- /dev/null +++ b/spec/support/email_format_shared_examples.rb @@ -0,0 +1,44 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +shared_examples 'an object with email-formated attributes' do |*attributes| + attributes.each do |attribute| + describe "specifically its :#{attribute} attribute" do + %w[ + info@example.com + info+test@example.com + o'reilly@example.com + mailto:test@example.com + lol!'+=?><#$%^&*()@gmail.com + ].each do |valid_email| + context "with a value of '#{valid_email}'" do + let(:email_value) { valid_email } + + it 'is valid' do + subject.send("#{attribute}=", valid_email) + + expect(subject).to be_valid + end + end + end + + %w[ + foobar + test@test@example.com + ].each do |invalid_email| + context "with a value of '#{invalid_email}'" do + let(:email_value) { invalid_email } + + it 'is invalid' do + subject.send("#{attribute}=", invalid_email) + + expect(subject).to be_invalid + end + end + end + end + end +end diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb new file mode 100644 index 0000000000000000000000000000000000000000..422083875d70ea7d12da17207928142196fca3cd --- /dev/null +++ b/spec/support/project_hook_data_shared_example.rb @@ -0,0 +1,27 @@ +RSpec.shared_examples 'project hook data' do |project_key: :project| + it 'contains project data' do + expect(data[project_key][:name]).to eq(project.name) + expect(data[project_key][:description]).to eq(project.description) + expect(data[project_key][:web_url]).to eq(project.web_url) + expect(data[project_key][:avatar_url]).to eq(project.avatar_url) + expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo) + expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:namespace]).to eq(project.namespace.name) + expect(data[project_key][:visibility_level]).to eq(project.visibility_level) + expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace) + expect(data[project_key][:default_branch]).to eq(project.default_branch) + expect(data[project_key][:homepage]).to eq(project.web_url) + expect(data[project_key][:url]).to eq(project.url_to_repo) + expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:http_url]).to eq(project.http_url_to_repo) + end +end + +RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project| + it 'contains deprecated repository data' do + expect(data[:repository][:name]).to eq(project.name) + expect(data[:repository][:description]).to eq(project.description) + expect(data[:repository][:url]).to eq(project.url_to_repo) + expect(data[:repository][:homepage]).to eq(project.web_url) + end +end