diff options
author | Johann Pardanaud <pardanaud.j@gmail.com> | 2016-02-16 19:53:51 +0100 |
---|---|---|
committer | Johann Pardanaud <pardanaud.j@gmail.com> | 2016-02-16 19:53:51 +0100 |
commit | fb6d7df347016726d84155d18b6d6f406d0121e9 (patch) | |
tree | 9529bd4b546feeff391ca5a9db0a445f76a9521b | |
parent | 78588cfca15348ba062d33061c79df3cfa6b9883 (diff) | |
parent | 7cc4b73942a0620678cd7e058d6dcde0ae71f4e6 (diff) | |
download | gitlab-ce-fb6d7df347016726d84155d18b6d6f406d0121e9.tar.gz |
Merge branch 'master' into avatar-cropping
Conflicts:
app/models/user.rb
189 files changed, 3026 insertions, 1269 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbdbae9d787..1c4d98ea3f6 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 988486e818d..74bc366d203 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 263f98b1e57..c4522998f42 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 844f6a91acb..d2b13eb644d 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.6.3 +0.6.4 @@ -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 a1e36c304e8..43c26396a30 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 eb951f71711..b2b8e1b7ffb 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 047df4786a9..360acb864f6 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 00000000000..a38a329c4c2 --- /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 dd295088312..62143e66cfe 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 d4a2b74b143..b17f8e51470 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 e7d884662ea..35b2fbbba07 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 ebf7140b7e3..eab34be652a 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 144852e7874..a61161810a3 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 9d5dc42b6cc..ef62f069dc2 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 1515086b16d..04a99d8c84a 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 a470d865408..fc342924987 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 48b1f95acb9..2c329b60a19 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 c420b59c3a2..5bb7d499cdc 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 3004c2d27f0..d1824b481d7 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 0bcc78a8bc7..2df6924b13d 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 2689bf4f1ec..a384f3004db 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 90475c17c17..ca5ce1e2046 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 f159a6d6dc6..cfea1266516 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 00000000000..a4dd94b941c --- /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 92d9699fe84..ec379c53b8f 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 5a084e123a1..36951b91372 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 bf5b54c8cb7..1420b96840c 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 7bbe75b3974..dc5d217f3e4 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 15506bd677a..a5c4ef1c7c7 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 e2785caa2fb..bedeb4a295c 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 4993b2648a5..0dd2d6a99be 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 30adfad1daa..92359745cec 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 10efafea9db..00234654578 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 4df5095bd94..14ca7426c2f 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 622cbfe3cc4..ecefa9b006d 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 1ed8c710f77..43a29c96bca 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 1a226252251..89d2a648494 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 00000000000..91a3aa371ef --- /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 43262d579e9..ae4ebc0854a 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 75f2ed5e054..29cb753e62c 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 dd49283089d..d6fb629b0c2 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 bc36434f549..41ae4048992 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 64c1ce8cfab..2f86d1be576 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 ab59a3506a2..a866eadeebb 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 9cafc78f761..269056e0e77 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 04650a9e67a..5136d0196a5 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 935705e2ed4..b323d1edd10 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 34efcd0088d..ca08007b7eb 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 ddc476447ca..1be8061e53d 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 c9a0ad8b9b6..9c4476c768e 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 043f08b9a13..a43878ebcad 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 3d7e8bbee61..e76d9eca2ab 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 c847eba8d1c..c96e6f0b8ea 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 e813c946bc1..ba275fd9803 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 a8e602f925a..1b0c82f45c4 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 f469b13e902..005a5c4661c 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 c0e08a151f2..707c2f7ff85 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 004b3ce7286..fae069ee4a5 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 d7ea30bc315..e3bf14966c8 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 1083bcec054..edced010811 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 b35af100803..aab07a7ece4 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 b4e3d96d405..b30dfd109ea 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -48,6 +48,16 @@ = 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 = f.text_field :admin_notification_email, class: 'form-control' diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 953b8b69368..5c9403fa0c2 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 00000000000..fbc9453c72e --- /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 c395bd908c3..34d955568f2 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 d46998ec1e9..4bc761b3738 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 66a4b535ae5..c248dbb695f 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 669079e9521..999a933390b 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 a829479bb38..9c16ab7e30f 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 ebb3df7dca3..a0ba11b11a1 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 00cb4aa24cc..12ded41fbf2 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 f4e9749e5c7..81d65037312 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 8b004d34cca..5d247eb4cf2 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 d3b799fca23..ad3ab2525bb 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 90ebdfc3fe2..23423e7d981 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 630d0286f25..5e3bd14565e 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 ba1fdc6f0e7..ca1441a20d8 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 329aaa0bb8b..befad27666c 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 2e3c956ddc4..a3449d1ae05 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 ede64d47ab3..c52cf25d40a 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 efc25eda26b..4ab81f3635c 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 058b71b21f5..4fcf7ea0b26 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 8a99aceef7f..fdcb6987471 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 acb2353d3ca..42fa6fdb782 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 121c775560f..fe977fd700c 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 3c5edf4b033..baaa2caa6de 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 e80dffc1ced..efe1e6f24c2 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 dcd61199717..6b77d24f50c 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 285af56ad73..627814bcfae 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: <code>https://username:password@gitlab.company.com/group/project.git</code>. %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 3d279ec228c..b07f1c5603e 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,21 +1,9 @@ -<svg width="36px" height="36px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="tanuki-logo"> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> - <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)"> - <g id="Page-1" sketch:type="MSShapeGroup"> - <g id="Fill-1-+-Group-24"> - <g id="Group-24"> - <g id="Group"> - <path id="tanuki-right-ear" d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" fill="#E24329" class="tanuki-shape"></path> - <path id="tanuki-right-cheek" d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path> - <path id="tanuki-right-eye" d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path> - <path id="tanuki-nose" d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" fill="#E24329" class="tanuki-shape"></path> - <path id="tanuki-left-eye" d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path> - <path id="tanuki-left-cheek" d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path> - <path id="tanuki-left-ear" d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" fill="#E24329" class="tanuki-shape"></path> - </g> - </g> - </g> - </g> - </g> - </g> +<svg width="36" height="36" id="tanuki-logo"> + <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> + <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> + <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> + <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/> + <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/> + <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/> + <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/> </svg> diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ae96a45453f..2aada5c9952 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -4,21 +4,18 @@ %span.issuable-count.pull-left = issuable.iid of - = issuable_count(:all, @project) + = issuables_count(issuable) %span.pull-right %a.gutter-toggle{href: '#'} - - if sidebar_gutter_collapsed? - = icon('angle-double-left') - - else - = icon('angle-double-right') + = sidebar_gutter_toggle_icon .issuable-nav.pull-right.btn-group{role: 'group', "aria-label" => '...'} - - if has_prev_issuable?(@project, issuable.id) - = link_to 'Prev', issuable_link_prev(@project, issuable), class: 'btn btn-default prev-btn' + - if prev_issuable = prev_issuable_for(issuable) + = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' - else %a.btn.btn-default.disabled{href: '#'} Prev - - if has_next_issuable?(@project, issuable.id) - = link_to 'Next', issuable_link_next(@project, issuable), class: 'btn btn-default next-btn' + - if next_issuable = next_issuable_for(issuable) + = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn' - else %a.btn.btn-default.disabled{href: '#'} Next @@ -118,7 +115,7 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon - = icon('clipboard') + = clipboard_button(clipboard_text: project_ref) .title .cross-project-reference %span diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 67edb264b7e..75684b972f1 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -21,9 +21,10 @@ #{projects_limit} of #{pluralize(projects.count, 'project')} displayed. = link_to '#', class: 'js-expand' do Show all - = paginate projects, theme: "gitlab" if !projects.kind_of?(Array) + = paginate projects, theme: "gitlab" if projects.respond_to? :total_pages - else %h3 No projects found :javascript new ProjectsList(); + Dashboard.init(); diff --git a/config/application.rb b/config/application.rb index d255ff0719f..1e9ec74cdbf 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,7 +31,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt) + config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -52,17 +52,11 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) - # Relative url support - # Uncomment and customize the last line to run in a non-root path - # WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this. - # Note that following settings need to be changed for this to work. - # 1) In your application.rb file: config.relative_url_root = "/gitlab" - # 2) In your gitlab.yml file: relative_url_root: /gitlab - # 3) In your unicorn.rb: ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" - # 4) In ../gitlab-shell/config.yml: gitlab_url: "http://127.0.0.1/gitlab" - # 5) In lib/support/nginx/gitlab : do not use asset gzipping, remove block starting with "location ~ ^/(assets)/" - # - # To update the path, run: sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production + # Relative URL support + # WARNING: We recommend using an FQDN to host GitLab in a root path instead + # of using a relative URL. + # Documentation: http://doc.gitlab.com/ce/install/relative_url.html + # Uncomment and customize the following line to run in a non-root path # # config.relative_url_root = "/gitlab" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d6e2c9380a5..faf05ecd466 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -38,8 +38,12 @@ production: &base # Otherwise, ssh host will be set to the `host:` value above # ssh_host: ssh.host_example.com - # WARNING: See config/application.rb under "Relative url support" for the list of - # other files that need to be changed for relative url support + # Relative URL support + # WARNING: We recommend using an FQDN to host GitLab in a root path instead + # of using a relative URL. + # Documentation: http://doc.gitlab.com/ce/install/relative_url.html + # Uncomment and customize the following line to run in a non-root path + # # relative_url_root: /gitlab # Uncomment and customize if you can't use the default user to run GitLab (default: 'git') diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index ec182502d4e..2287a76fca7 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -12,7 +12,7 @@ if Rails.env.production? ActionMailer::Base.smtp_settings = { address: "email.server.com", - port: 456, + port: 465, user_name: "smtp", password: "123456", domain: "gitlab.company.com", diff --git a/config/routes.rb b/config/routes.rb index 034bfaf1bcd..507bcbc53d7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -227,7 +227,10 @@ Rails.application.routes.draw do get :test end - resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] + resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do + post :preview, on: :collection + end + resource :logs, only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] @@ -349,6 +352,7 @@ Rails.application.routes.draw do get :issues get :merge_requests get :projects + get :events end scope module: :groups do @@ -604,7 +608,7 @@ Rails.application.routes.draw do resource :variables, only: [:show, :update] resources :triggers, only: [:index, :create, :destroy] - resources :builds, only: [:index, :show] do + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all end @@ -693,6 +697,12 @@ Rails.application.routes.draw do end resources :runner_projects, only: [:create, :destroy] + resources :badges, only: [], path: 'badges/*ref', + constraints: { ref: Gitlab::Regex.git_reference_regex } do + collection do + get :build, constraints: { format: /svg/ } + end + end end end end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index b937b092789..e5058cebce8 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -10,9 +10,12 @@ # Note: If you change this file in a Merge Request, please also create a # Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests -# -# WARNING: See config/application.rb under "Relative url support" for the list of -# other files that need to be changed for relative url support + +# Relative URL support +# WARNING: We recommend using an FQDN to host GitLab in a root path instead +# of using a relative URL. +# Documentation: http://doc.gitlab.com/ce/install/relative_url.html +# Uncomment and customize the following line to run in a non-root path # # ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index 03a12323845..e3ca2b4eea3 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -1,24 +1,13 @@ class Gitlab::Seeder::Builds - BUILD_STATUSES = %w(running pending success failed canceled) - def initialize(project) @project = project end def seed! ci_commits.each do |ci_commit| - build = Ci::Build.new(build_attributes_for(ci_commit)) - - artifacts_cache_file(artifacts_archive_path) do |file| - build.artifacts_file = file - end - - artifacts_cache_file(artifacts_metadata_path) do |file| - build.artifacts_metadata = file - end - begin - build.save! + build_create!(ci_commit, name: 'test build 1') + build_create!(ci_commit, status: 'success', name: 'test build 2') print '.' rescue ActiveRecord::RecordInvalid print 'F' @@ -36,6 +25,28 @@ class Gitlab::Seeder::Builds [] end + def build_create!(ci_commit, opts = {}) + attributes = build_attributes_for(ci_commit).merge(opts) + build = Ci::Build.new(attributes) + + if %w(success failed).include?(build.status) + artifacts_cache_file(artifacts_archive_path) do |file| + build.artifacts_file = file + end + + artifacts_cache_file(artifacts_metadata_path) do |file| + build.artifacts_metadata = file + end + end + + build.save! + + if %w(running success failed).include?(build.status) + # We need to set build trace after saving a build (id required) + build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") + end + end + def build_attributes_for(ci_commit) { name: 'test build', commands: "$ build command", stage: 'test', stage_idx: 1, ref: 'master', @@ -49,7 +60,7 @@ class Gitlab::Seeder::Builds end def build_status - BUILD_STATUSES.sample + Ci::Build::AVAILABLE_STATUSES.sample end def artifacts_archive_path diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index b0c0b6450f6..308b0528c9b 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -6,8 +6,10 @@ else expire_time = nil end +email = ENV['GITLAB_ROOT_EMAIL'].presence || 'admin@example.com' + admin = User.create( - email: "admin@example.com", + email: email, name: "Administrator", username: 'root', password: password, diff --git a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb new file mode 100644 index 00000000000..d50791410f9 --- /dev/null +++ b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddEmailAuthorInBodyToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :email_author_in_body, :boolean, default: false + end +end diff --git a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb index 091de54978b..d3ea956952e 100644 --- a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb +++ b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb @@ -48,7 +48,7 @@ class RemoveDotAtomPathEndingOfProjects < ActiveRecord::Migration end def projects_with_dot_atom - select_all("SELECT p.id, p.path, n.path as namespace_path, n.id as namespace_id FROM projects p inner join namespaces n on n.id = p.namespace_id WHERE lower(p.path) LIKE '%.atom'") + select_all("SELECT p.id, p.path, n.path as namespace_path, n.id as namespace_id FROM projects p inner join namespaces n on n.id = p.namespace_id WHERE p.path LIKE '%.atom'") end def up diff --git a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb new file mode 100644 index 00000000000..793984343b4 --- /dev/null +++ b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb @@ -0,0 +1,5 @@ +class AddAllowGuestToAccessBuildsProject < ActiveRecord::Migration + def change + add_column :projects, :public_builds, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20160209130428_add_index_to_snippet.rb b/db/migrate/20160209130428_add_index_to_snippet.rb new file mode 100644 index 00000000000..95d5719be59 --- /dev/null +++ b/db/migrate/20160209130428_add_index_to_snippet.rb @@ -0,0 +1,5 @@ +class AddIndexToSnippet < ActiveRecord::Migration + def change + add_index :snippets, :updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index d4710346b82..689a8c3ecc5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160129135155) do +ActiveRecord::Schema.define(version: 20160209130428) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -66,6 +66,7 @@ ActiveRecord::Schema.define(version: 20160129135155) do t.string "sentry_dsn" t.boolean "akismet_enabled", default: false t.string "akismet_api_key" + t.boolean "email_author_in_body", default: false end create_table "audit_events", force: :cascade do |t| @@ -680,6 +681,7 @@ ActiveRecord::Schema.define(version: 20160129135155) do t.boolean "build_allow_git_fetch", default: true, null: false t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false + t.boolean "public_builds", default: true, null: false end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -770,6 +772,7 @@ ActiveRecord::Schema.define(version: 20160129135155) do add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree + add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree create_table "spam_logs", force: :cascade do |t| diff --git a/doc/api/builds.md b/doc/api/builds.md index 6e64d096644..43edb40e911 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -8,20 +8,16 @@ Get a list of builds in a project. GET /projects/:id/builds ``` -### Parameters - -| Attribute | Type | required | Description | +| Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| -| id | integer | yes | The ID of a project | -| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | - -### Example of request +| `id` | integer | yes | The ID of a project | +| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" ``` -### Example of response +Example of response ```json [ @@ -112,21 +108,17 @@ Get a list of builds for specific commit in a project. GET /projects/:id/repository/commits/:sha/builds ``` -### Parameters - -| Attribute | Type | required | Description | +| Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| -| id | integer | yes | The ID of a project | -| sha | string | yes | The SHA id of a commit | -| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | - -### Example of request +| `id` | integer | yes | The ID of a project | +| `sha` | string | yes | The SHA id of a commit | +| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | ``` curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" ``` -### Example of response +Example of response ```json [ @@ -203,20 +195,16 @@ Get a single build of a project GET /projects/:id/builds/:build_id ``` -### Parameters - -| Attribute | Type | required | Description | -|-----------|---------|----------|---------------------| -| id | integer | yes | The ID of a project | -| build\_id | integer | yes | The ID of a build | - -### Example of request +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `build_id` | integer | yes | The ID of a build | ``` curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8" ``` -### Example of response +Example of response ```json { @@ -267,20 +255,16 @@ Cancel a single build of a project POST /projects/:id/builds/:build_id/cancel ``` -### Parameters - -| Attribute | Type | required | Description | -|-----------|---------|----------|---------------------| -| id | integer | yes | The ID of a project | -| build\_id | integer | yes | The ID of a build | - -### Example of request +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `build_id` | integer | yes | The ID of a build | ``` curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel" ``` -### Example of response +Example of response ```json { @@ -317,20 +301,16 @@ Retry a single build of a project POST /projects/:id/builds/:build_id/retry ``` -### Parameters - -| Attribute | Type | required | Description | -|-----------|---------|----------|---------------------| -| id | integer | yes | The ID of a project | -| build\_id | integer | yes | The ID of a build | - -### Example of request +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | +| `build_id` | integer | yes | The ID of a build | ``` curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry" ``` -### Example of response +Example of response ```json { diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 85ed31320b9..009532b50c1 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -2,7 +2,7 @@ ## List merge requests -Get all merge requests for this project. +Get all merge requests for this project. The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. @@ -49,8 +49,24 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, + "source_project_id": "2", + "target_project_id": "3", + "labels": [ ], "description":"fixed login page css paddings", - "work_in_progress": false + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 3, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ] ``` @@ -95,8 +111,24 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, + "source_project_id": "2", + "target_project_id": "3", + "labels": [ ], "description":"fixed login page css paddings", - "work_in_progress": false + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 3, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ``` @@ -156,8 +188,6 @@ Parameters: "iid": 1, "project_id": 4, "title": "Blanditiis beatae suscipit hic assumenda et molestias nisi asperiores repellat et.", - "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.", - "work_in_progress": false, "state": "reopened", "created_at": "2015-02-02T19:49:39.159Z", "updated_at": "2015-02-02T20:08:49.959Z", @@ -182,6 +212,8 @@ Parameters: "source_project_id": 4, "target_project_id": 4, "labels": [ ], + "description": "Qui voluptatibus placeat ipsa alias quasi. Deleniti rem ut sint. Optio velit qui distinctio.", + "work_in_progress": false, "milestone": { "id": 5, "iid": 1, @@ -193,6 +225,8 @@ Parameters: "updated_at": "2015-02-02T19:49:26.013Z", "due_date": null }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged", "changes": [ { "old_path": "VERSION", @@ -225,6 +259,7 @@ Parameters: - `description` (optional) - Description of MR - `target_project_id` (optional) - The target project (numeric id) - `labels` (optional) - Labels for MR as a comma-separated list +- `milestone_id` (optional) - Milestone ID ```json { @@ -252,7 +287,24 @@ Parameters: "state": "active", "created_at": "2012-04-29T08:46:00Z" }, - "description":"fixed login page css paddings" + "source_project_id": 4, + "target_project_id": 4, + "labels": [ ], + "description":"fixed login page css paddings", + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 4, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ``` @@ -277,6 +329,7 @@ Parameters: - `description` - Description of MR - `state_event` - New state (close|reopen|merge) - `labels` (optional) - Labels for MR as a comma-separated list +- `milestone_id` (optional) - Milestone ID ```json { @@ -284,7 +337,6 @@ Parameters: "target_branch": "master", "project_id": 3, "title": "test1", - "description": "description1", "state": "opened", "upvotes": 0, "downvotes": 0, @@ -303,7 +355,25 @@ Parameters: "name": "Administrator", "state": "active", "created_at": "2012-04-29T08:46:00Z" - } + }, + "source_project_id": 4, + "target_project_id": 4, + "labels": [ ], + "description": "description1", + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 4, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ``` @@ -359,7 +429,25 @@ Parameters: "name": "Administrator", "state": "active", "created_at": "2012-04-29T08:46:00Z" - } + }, + "source_project_id": 4, + "target_project_id": 4, + "labels": [ ], + "description":"fixed login page css paddings", + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 4, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ``` @@ -387,7 +475,7 @@ Parameters: "source_branch": "test1", "project_id": 3, "title": "test1", - "state": "merged", + "state": "opened", "upvotes": 0, "downvotes": 0, "author": { @@ -405,10 +493,90 @@ Parameters: "name": "Administrator", "state": "active", "created_at": "2012-04-29T08:46:00Z" - } + }, + "source_project_id": 4, + "target_project_id": 4, + "labels": [ ], + "description":"fixed login page css paddings", + "work_in_progress": false, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 4, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": null + }, + "merge_when_build_succeeds": true, + "merge_status": "can_be_merged" } ``` ## Comments on merge requets Comments are done via the [notes](notes.md) resource. + +## List issues that will close on merge + +Get all the issues that would be closed by merging the provided merge request. + +``` +GET /projects/:id/merge_requests/:merge_request_id/closes_issues +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of the merge request | + +```bash +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues +``` + +Example response: + +```json +[ + { + "state" : "opened", + "description" : "Ratione dolores corrupti mollitia soluta quia.", + "author" : { + "state" : "active", + "id" : 18, + "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "name" : "Alexandra Bashirian", + "avatar_url" : null, + "username" : "eileen.lowe" + }, + "milestone" : { + "project_id" : 1, + "description" : "Ducimus nam enim ex consequatur cumque ratione.", + "state" : "closed", + "due_date" : null, + "iid" : 2, + "created_at" : "2016-01-04T15:31:39.996Z", + "title" : "v4.0", + "id" : 17, + "updated_at" : "2016-01-04T15:31:39.996Z" + }, + "project_id" : 1, + "assignee" : { + "state" : "active", + "id" : 1, + "name" : "Administrator", + "web_url" : "https://gitlab.example.com/u/root", + "avatar_url" : null, + "username" : "root" + }, + "updated_at" : "2016-01-04T15:31:51.081Z", + "id" : 76, + "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", + "created_at" : "2016-01-04T15:31:51.081Z", + "iid" : 6, + "labels" : [] + }, +] +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 3f372c955d2..9e9486cd87a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -80,7 +80,9 @@ Parameters: "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", "shared_runners_enabled": true, "forks_count": 0, - "star_count": 0 + "star_count": 0, + "runners_token": "b8547b1dc37721d05889db52fa2f02", + "public_builds": true }, { "id": 6, @@ -137,7 +139,8 @@ Parameters: "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, - "runners_token": "b8547b1dc37721d05889db52fa2f02" + "runners_token": "b8547b1dc37721d05889db52fa2f02", + "public_builds": true } ] ``` @@ -424,6 +427,7 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) +- `public_builds` (optional) ### Create project for user @@ -446,6 +450,7 @@ Parameters: - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) +- `public_builds` (optional) ### Edit project @@ -469,6 +474,7 @@ Parameters: - `snippets_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) +- `public_builds` (optional) On success, method returns 200 with the updated project. If parameters are invalid, 400 is returned. diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 63fe840b369..bd748f1b986 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -270,7 +270,7 @@ This will forcefully (`-f`) remove the `build` container, the two service containers as well as all volumes (`-v`) that were created with the container creation. -[Docker Fundamentals]: https://docs.docker.com/engine/introduction/understanding-docker/ +[Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/ [hub]: https://hub.docker.com/ [linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ [tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/ diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index a9b36139de9..ae7b760fa67 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -184,6 +184,14 @@ you expected. You are also able to view the status of any commit in the various pages in GitLab, such as **Commits** and **Merge Requests**. +## Builds badge + +You can access a builds badge image using following link: + +``` +http://example.gitlab.com/namespace/project/badges/branch/build.svg +``` + ## Next steps Awesome! You started using CI in GitLab! diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 862cacda586..018d1898594 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -77,9 +77,12 @@ More information about Docker integration can be found in [Using Docker Images]( GitLab CI allows you to define per-project **Secure Variables** that are set in build environment. The secure variables are stored out of the repository (the `.gitlab-ci.yml`). -These variables are securely stored in GitLab CI database and are hidden in the build log. +The variables are securely passed to GitLab Runner and are available in build environment. It's desired method to use them for storing passwords, secret keys or whatever you want. +**The value of the variable can be shown in build log if explicitly asked to do so.** +If your project is public or internal you can make the builds private. + Secure Variables can added by going to `Project > Variables > Add Variable`. They will be available for all subsequent builds. diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 4fa1961fde9..4e108c17871 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -8,6 +8,8 @@ In addition, having to take a server offline for a an upgrade small or big is a big burden for most organizations. For this reason it is important that your migrations are written carefully, can be applied online and adhere to the style guide below. +It's advised to have offline migrations only in major GitLab releases. + When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as little assumptions as possible about the state of the database. @@ -33,6 +35,8 @@ It is always preferable to have a migration run online. If you expect the migrat to take particularly long (for instance, if it loops through all notes), this is valuable information to add. +If you don't provide the information it means that a migration is safe to run online. + ### Reversibility Your migration should be reversible. This is very important, as it should @@ -85,4 +89,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") end -``` +```
\ No newline at end of file diff --git a/doc/install/installation.md b/doc/install/installation.md index 3eb9b1767c5..7d3f9d0a2ed 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -238,9 +238,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-4-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-5-stable gitlab -**Note:** You can change `8-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -358,7 +358,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout 0.6.3 + sudo -u git -H git checkout 0.6.4 sudo -u git -H make ### Initialize Database and Activate Advanced Features @@ -373,9 +373,9 @@ GitLab Shell is an SSH access and repository management software developed speci # When done you see 'Administrator account created:' -**Note:** You can set the Administrator/root password by supplying it in environmental variable `GITLAB_ROOT_PASSWORD` as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password. +**Note:** You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password. - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword + sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail ### Secure secrets.yml @@ -484,6 +484,11 @@ You can use `sudo service gitlab start` and `sudo service gitlab stop` to start ## Advanced Setup Tips +### Relative URL support + +See the [Relative URL documentation](relative_url.md) for more information on +how to configure GitLab with a relative URL. + ### Using HTTPS To use GitLab with HTTPS: diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md new file mode 100644 index 00000000000..b341b6311c6 --- /dev/null +++ b/doc/install/relative_url.md @@ -0,0 +1,126 @@ +## Install GitLab under a relative URL + +_**Note:** +This document describes how to run GitLab under a relative URL for installations +from source. If you are using an Omnibus package, +[the steps are different][omnibus-rel]. Use this guide along with the +[installation guide](installation.md) if you are installing GitLab for the +first time._ + +--- + +While it is recommended to install GitLab on its own (sub)domain, sometimes +this is not possible due to a variety of reasons. In that case, GitLab can also +be installed under a relative URL, for example `https://example.com/gitlab`. + +There is no limit to how deeply nested the relative URL can be. For example you +could serve GitLab under `/foo/bar/gitlab/git` without any issues. + +Note that by changing the URL on an existing GitLab installation, all remote +URLs will change, so you'll have to manually edit them in any local repository +that points to your GitLab instance. + +--- + +The TL;DR list of configuration files that you need to change in order to +serve GitLab under a relative URL is: + +- `/home/git/gitlab/config/application.rb` +- `/home/git/gitlab/config/gitlab.yml` +- `/home/git/gitlab/config/unicorn.rb` +- `/home/git/gitlab-shell/config.yml` +- `/etc/default/gitlab` + +After all the changes you need to recompile the assets and [restart GitLab]. + +### Relative URL requirements + +If you configure GitLab with a relative URL, the assets (JavaScript, CSS, fonts, +images, etc.) will need to be recompiled, which is a task which consumes a lot +of CPU and memory resources. To avoid out-of-memory errors, you should have at +least 2GB of RAM available on your system, while we recommend 4GB RAM, and 4 or +8 CPU cores. + +See the [requirements](requirements.md) document for more information. + +### Enable relative URL in GitLab + +_**Note:** +Do not make any changes to your web server configuration file regarding +relative URL. The relative URL support is implemented by GitLab Workhorse._ + +--- + +Before following the steps below to enable relative URL in GitLab, some +assumptions are made: + +- GitLab is served under `/gitlab` +- The directory under which GitLab is installed is `/home/git/` + +Make sure to follow all steps below: + +1. (Optional) If you run short on resources, you can temporarily free up some + memory by shutting down the GitLab service with the following command: + + ```shell + sudo service gitlab stop + ``` + +1. Edit `/home/git/gitlab/config/application.rb` and uncomment/change the + following line: + + ```ruby + config.relative_url_root = "/gitlab" + ``` + +1. Edit `/home/git/gitlab/config/gitlab.yml` and uncomment/change the + following line: + + ```yaml + relative_url_root: /gitlab + ``` + +1. Edit `/home/git/gitlab/config/unicorn.rb` and uncomment/change the + following line: + + ```ruby + ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" + ``` + +1. Edit `/home/git/gitlab-shell/config.yml` and append the relative path to + the following line: + + ```yaml + gitlab_url: http://127.0.0.1/gitlab + ``` + +1. Make sure you have copied the supplied init script and the defaults file + as stated in the [installation guide](installation.md#install-init-script). + Then, edit `/etc/default/gitlab` and set in `gitlab_workhorse_options` the + `-authBackend` setting to read like: + + ```shell + -authBackend http://127.0.0.1:8080/gitlab + ``` + + **Note:** + If you are using a custom init script, make sure to edit the above + gitlab-workhorse setting as needed. + +1. After all the above changes recompile the assets. This is an important task + and will take some time to complete depending on the server resources: + + ``` + cd /home/git/gitlab + sudo -u git -H bundle exec rake assets:clean assets:precompile RAILS_ENV=production + ``` + +1. [Restart GitLab][] for the changes to take effect. + +### Disable relative URL in GitLab + +To disable the relative URL, follow the same steps as above and set up the +GitLab URL to one that doesn't contain a relative path. + +[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" +[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/markdown/img/logo.png b/doc/markdown/img/logo.png Binary files differnew file mode 100644 index 00000000000..7da5f23ed9b --- /dev/null +++ b/doc/markdown/img/logo.png diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index f6bffba9fdb..c400cdac64d 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -424,24 +424,24 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown Here's our logo (hover to see the title text): Inline-style: - ![alt text](assets/logo.svg) + ![alt text](img/logo.png) Reference-style: ![alt text1][logo] - [logo]: assets/logo.svg + [logo]: img/logo.png Here's our logo: Inline-style: -![alt text](/assets/logo.svg) +![alt text](img/logo.png) Reference-style: ![alt text][logo] -[logo]: /assets/logo.svg +[logo]: img/logo.png ## Blockquotes diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 1be78ac1823..168e7d143ee 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -18,6 +18,9 @@ documentation](../workflow/add-user/add-user.md). |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | +| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Pull project code | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | @@ -31,6 +34,7 @@ documentation](../workflow/add-user/add-user.md). | Remove non-protected branches | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | | Write a wiki | | | ✓ | ✓ | ✓ | +| Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | @@ -40,12 +44,17 @@ documentation](../workflow/add-user/add-user.md). | Edit project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ | | Configure project hooks | | | | ✓ | ✓ | +| Manage runners | | | | ✓ | ✓ | +| Manage build triggers | | | | ✓ | ✓ | +| Manage variables | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Force push to protected branches | | | | | | | Remove protected branches | | | | | | +[^1]: If **Allow guest to access builds** is enabled in CI settings + ## Group In order for a group to appear as public and be browsable, it must contain at diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index cdd6652b7b0..f6d1234ac4a 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -18,8 +18,6 @@ for two-factor authentication. If you restore a GitLab backup without restoring the database encryption key, users who have two-factor authentication enabled will lose access to your GitLab server. -If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)* - ``` # use this command if you've installed GitLab with the Omnibus package sudo gitlab-rake gitlab:backup:create diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md new file mode 100644 index 00000000000..42b26439848 --- /dev/null +++ b/doc/update/8.4-to-8.5.md @@ -0,0 +1,116 @@ +# From 8.4 to 8.5 + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-5-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all +sudo -u git -H git checkout v2.6.10 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout 0.6.4 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-4-stable:config/gitlab.yml.example origin/8-5-stable:config/gitlab.yml.example +``` + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.4) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.3 to 8.4](8.3-to-8.4.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c29037e89c2..b82306bd1da 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,5 +1,12 @@ # Web hooks +_**Note:** +Starting from GitLab 8.5:_ + +- _the `repository` key is deprecated in favor of the `project` key_ +- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_ +- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_ + Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created. You can configure web hooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the web hook URL. @@ -8,8 +15,8 @@ Web hooks can be used to update an external issue tracker, trigger CI builds, up ## SSL Verification -By default, the SSL certificate of the webhook endpoint is verified based on -an internal list of Certificate Authorities, +By default, the SSL certificate of the webhook endpoint is verified based on +an internal list of Certificate Authorities, which means the certificate cannot be self-signed. You can turn this off in the web hook settings in your GitLab projects. @@ -37,8 +44,25 @@ X-Gitlab-Event: Push Hook "user_id": 4, "user_name": "John Smith", "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 15, - "repository": { + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diasporadiaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ "name": "Diaspora", "url": "git@example.com:mike/diasporadiaspora.git", "description": "", @@ -76,7 +100,6 @@ X-Gitlab-Event: Push Hook } ], "total_commits_count": 4 - } ``` @@ -101,8 +124,25 @@ X-Gitlab-Event: Tag Push Hook "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", "user_id": 1, "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", "project_id": 1, - "repository": { + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ "name": "jsmith", "url": "ssh://git@example.com/jsmith/example.git", "description": "", @@ -136,7 +176,23 @@ X-Gitlab-Event: Issue Hook "username": "root", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", "url": "http://example.com/gitlabhq/gitlab-test.git", "description": "Aut reprehenderit ut est.", @@ -158,6 +214,11 @@ X-Gitlab-Event: Issue Hook "iid": 23, "url": "http://example.com/diaspora/issues/23", "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" } } ``` @@ -193,9 +254,25 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", - "url": "http://localhost/gitlab-org/gitlab-test.git", + "url": "http://example.com/gitlab-org/gitlab-test.git", "description": "Aut reprehenderit ut est.", "homepage": "http://example.com/gitlab-org/gitlab-test" }, @@ -256,9 +333,25 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", + "url": "http://localhost/gitlab-org/gitlab-test.git", "description": "Aut reprehenderit ut est.", "homepage": "http://example.com/gitlab-org/gitlab-test" }, @@ -296,21 +389,37 @@ X-Gitlab-Event: Note Hook "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", "position": 0, "locked_at": null, - "source": { - "name": "Gitlab Test", - "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", - "http_url": "http://example.com/gitlab-org/gitlab-test.git", - "web_url": "http://example.com/gitlab-org/gitlab-test", - "namespace": "Gitlab Org", - "visibility_level": 10 + "source":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" }, "target": { - "name": "Gitlab Test", - "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", - "http_url": "http://example.com/gitlab-org/gitlab-test.git", - "web_url": "http://example.com/gitlab-org/gitlab-test", - "namespace": "Gitlab Org", - "visibility_level": 10 + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" }, "last_commit": { "id": "562e173be03b8ff2efb05345d12df18815438a4b", @@ -322,7 +431,12 @@ X-Gitlab-Event: Note Hook "email": "john@example.com" } }, - "work_in_progress": false + "work_in_progress": false, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } } } ``` @@ -346,11 +460,27 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"diaspora", + "url":"git@example.com:mike/diasporadiaspora.git", + "description":"", + "homepage":"http://example.com/mike/diaspora" }, "object_attributes": { "id": 1241, @@ -388,7 +518,6 @@ X-Gitlab-Event: Note Hook ### Comment on code snippet - **Request header**: ``` @@ -397,7 +526,7 @@ X-Gitlab-Event: Note Hook **Request body:** -``` +```json { "object_kind": "note", "user": { @@ -406,11 +535,27 @@ X-Gitlab-Event: Note Hook "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" }, "project_id": 5, - "repository": { - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"Gitlab Test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "description":"Aut reprehenderit ut est.", + "homepage":"http://example.com/gitlab-org/gitlab-test" }, "object_attributes": { "id": 1245, @@ -482,21 +627,37 @@ X-Gitlab-Event: Merge Request Hook "target_project_id": 14, "iid": 1, "description": "", - "source": { - "name": "awesome_project", - "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git", - "http_url": "http://example.com/awesome_space/awesome_project.git", - "web_url": "http://example.com/awesome_space/awesome_project", - "visibility_level": 20, - "namespace": "awesome_space" + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" }, "target": { - "name": "awesome_project", - "ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git", - "http_url": "http://example.com/awesome_space/awesome_project.git", - "web_url": "http://example.com/awesome_space/awesome_project", - "visibility_level": 20, - "namespace": "awesome_space" + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" }, "last_commit": { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", @@ -510,7 +671,12 @@ X-Gitlab-Event: Merge Request Hook }, "work_in_progress": false, "url": "http://example.com/diaspora/merge_requests/1", - "action": "open" + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } } } ``` diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index be32f0c720b..0b205ea6de7 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -187,12 +187,15 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. +In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. When you rebase you change the identifier (SHA-1) of the commit and this is confusing. If you do that the same change will be known under multiple identifiers and this can cause much confusion. If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit. +Another reasons not to rebase is that you lose authorship information, maybe someone created a merge request, another person pushed a commit on there to improve it and a third one merged it. +In this case rebasing all the commits into one prevent the other authors from being properly attributed and sharing part of the [git blame](https://git-scm.com/docs/git-blame). People are encouraged to commit often and to frequently push to the remote repository so other people are aware what everyone is working on. This will lead to many commits per change which makes the history harder to understand. @@ -221,13 +224,11 @@ You can reuse recorded resolutions (rerere) sometimes, but without rebasing you There has to be a better way to avoid many merge commits. The way to prevent creating many merge commits is to not frequently merge master into the feature branch. -We'll discuss the three reasons to merge in master: leveraging code, solving merge conflicts and long running branches. +We'll discuss the three reasons to merge in master: leveraging code, merge conflicts, and long running branches. If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit. If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this. -You should aim to prevent merge conflicts where they are likely to occur. -One example is the CHANGELOG file where each significant change in the codebase is documented under a version header. -Instead of everyone adding their change at the bottom of the list for the current version it is better to randomly insert it in the current list for that version. -This it is likely that multiple feature branches that add to the CHANGELOG can be merged before a conflict occurs. +You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order. +For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG merge=union` so that there are fewer merge conflicts in it. The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project. Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI). At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit. diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature index fd3bac77f86..4f9c651561e 100644 --- a/features/admin/broadcast_messages.feature +++ b/features/admin/broadcast_messages.feature @@ -25,3 +25,9 @@ Feature: Admin Broadcast Messages When I remove an existing broadcast message Then I should be redirected to admin messages page And I should not see the removed broadcast message + + @javascript + Scenario: Live preview a customized broadcast message + When I visit admin messages page + And I enter a broadcast message with Markdown + Then I should see a live preview of the rendered broadcast message diff --git a/features/project/badges/build.feature b/features/project/badges/build.feature new file mode 100644 index 00000000000..9417f62d680 --- /dev/null +++ b/features/project/badges/build.feature @@ -0,0 +1,22 @@ +Feature: Project Badges Build + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I want to see a badge for successfully built project + Given recent build is successful + When I display builds badge for a master branch + Then I should see a build success badge + + Scenario: I want to see a badge for project with failed builds + Given recent build failed + When I display builds badge for a master branch + Then I should see a build failed badge + + Scenario: I want to see a badge for project with running builds + Given recent build is successful + And project has another build that is running + When I display builds badge for a master branch + Then I should see a build running badge diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature index 1193bcd74f6..3c7f72335d9 100644 --- a/features/project/builds/permissions.feature +++ b/features/project/builds/permissions.feature @@ -5,6 +5,41 @@ Feature: Project Builds Permissions And project has CI enabled And project has a recent build + Scenario: I try to visit build details as guest + Given I am member of a project with a guest role + When I visit recent build details page + Then page status code should be 404 + + Scenario: I try to visit project builds page as guest + Given I am member of a project with a guest role + When I visit project builds page + Then page status code should be 404 + + Scenario: I try to visit build details of internal project without access to builds + Given The project is internal + And public access for builds is disabled + When I visit recent build details page + Then page status code should be 404 + + Scenario: I try to visit internal project builds page without access to builds + Given The project is internal + And public access for builds is disabled + When I visit project builds page + Then page status code should be 404 + + Scenario: I try to visit build details of internal project with access to builds + Given The project is internal + And public access for builds is enabled + When I visit recent build details page + Then I see details of a build + And I see build trace + + Scenario: I try to visit internal project builds page with access to builds + Given The project is internal + And public access for builds is enabled + When I visit project builds page + Then I see the build + Scenario: I try to download build artifacts as guest Given I am member of a project with a guest role And recent build has artifacts available diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 01c10721312..a95df038357 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -7,6 +7,26 @@ Feature: Project Commits Scenario: I browse commits list for master branch Then I see project commits + And I should not see button to create a new merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + + Scenario: I browse commits list for feature branch without a merge request + Given I visit commits list page for feature branch + Then I see feature branch commits + And I see button to create a new merge request + Then I click the "Compare" tab + And I see button to create a new merge request + + Scenario: I browse commits list for feature branch with an open merge request + Given project have an open merge request + And I visit commits list page for feature branch + Then I see feature branch commits + And I should not see button to create a new merge request + And I should see button to the merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + And I should see button to the merge request Scenario: I browse atom feed of commits list for master branch Given I click atom feed link @@ -31,6 +51,22 @@ Feature: Project Commits Then I see inline diff button @javascript + Scenario: I compare branches without a merge request + Given I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I see button to create a new merge request + + @javascript + Scenario: I compare branches with an open merge request + Given project have an open merge request + And I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I should not see button to create a new merge request + And I should see button to the merge request + + @javascript Scenario: I compare refs Given I visit compare refs page And I fill compare fields with refs diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index bfde89fd896..2945bb3753a 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -7,7 +7,16 @@ Feature: Award Emoji And I visit "Bugfix" issue page @javascript - Scenario: I add and remove award in the issue + Scenario: I repeatedly add and remove thumbsup award in the issue + Given I click the thumbsup award Emoji + Then I have award added + Given I click the thumbsup award Emoji + Then I have no awards added + Given I click the thumbsup award Emoji + Then I have award added + + @javascript + Scenario: I add and remove custom award in the issue Given I click to emoji-picker Then The search field is focused And I click to emoji in the picker diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 0b3d03aa2a5..ca2399d85a9 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,9 +25,16 @@ Feature: Project Issues Scenario: I visit issue page Given I click link "Release 0.4" Then I should see issue "Release 0.4" + And I should see "1 of 2" in the sidebar + + Scenario: I navigate between issues + Given I click link "Release 0.4" + Then I click link "Next" in the sidebar + Then I should see issue "Tweet control" + And I should see "2 of 2" in the sidebar @javascript - Scenario: I visit issue page + Scenario: I filter by author Given I add a user to project "Shop" And I click "author" dropdown Then I see current user as the first user diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index ca1ee6b3c2b..5995e787961 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -39,6 +39,7 @@ Feature: Project Merge Requests Scenario: I visit merge request page Given I click link "Bug NS-04" Then I should see merge request "Bug NS-04" + And I should see "1 of 1" in the sidebar Scenario: I close merge request page Given I click link "Bug NS-04" diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb index 6cacdf4764c..af2b4a29313 100644 --- a/features/steps/admin/broadcast_messages.rb +++ b/features/steps/admin/broadcast_messages.rb @@ -19,7 +19,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps end step 'submit form with new customized broadcast message' do - fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST' + fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**' fill_in 'broadcast_message_color', with: '#f2dede' fill_in 'broadcast_message_font', with: '#b94a48' select Date.today.next_year.year, from: "broadcast_message_ends_at_1i" @@ -28,6 +28,7 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps step 'I should see a customized broadcast message' do expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST' + expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST' expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"]) end @@ -51,4 +52,15 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps step 'I should not see the removed broadcast message' do expect(page).not_to have_content 'Migration to new server' end + + step 'I enter a broadcast message with Markdown' do + fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:" + end + + step 'I should see a live preview of the rendered broadcast message' do + page.within('.broadcast-message-preview') do + expect(page).to have_selector('strong', text: 'Markdown') + expect(page).to have_selector('img.emoji') + end + end end diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb new file mode 100644 index 00000000000..cbfc35bed65 --- /dev/null +++ b/features/steps/project/badges/build.rb @@ -0,0 +1,28 @@ +class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + + step 'I display builds badge for a master branch' do + visit build_namespace_project_badges_path(@project.namespace, @project, ref: :master, format: :svg) + end + + step 'I should see a build success badge' do + expect_badge('success') + end + + step 'I should see a build failed badge' do + expect_badge('failed') + end + + step 'I should see a build running badge' do + expect_badge('running') + end + + def expect_badge(status) + svg = Nokogiri::XML.parse(page.body) + expect(page.response_headers).to include('Content-Type' => 'image/svg+xml') + expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy + end +end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 4bc670fdfcb..4f94fc96354 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -4,14 +4,6 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps include SharedBuilds include RepoHelpers - step 'I see details of a build' do - expect(page).to have_content "Build ##{@build.id}" - end - - step 'I see build trace' do - expect(page).to have_css '#build-trace' - end - step 'I see button to CI Lint' do page.within('.nav-controls') do ci_lint_tool_link = page.find_link('CI Lint') diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index daf6cdaaac8..f9fd7332464 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -33,6 +33,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files" end + step 'I fill compare fields with branches' do + fill_in 'from', with: 'feature' + fill_in 'to', with: 'master' + + click_button 'Compare' + end + step 'I fill compare fields with refs' do fill_in "from", with: sample_commit.parent_id fill_in "to", with: sample_commit.id @@ -56,6 +63,56 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Showing 2 changed files" end + step 'I visit commits list page for feature branch' do + visit namespace_project_commits_path(@project.namespace, @project, 'feature', { limit: 5 }) + end + + step 'I see feature branch commits' do + commit = @project.repository.commit('0b4bc9a') + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..12]) + expect(page).to have_content(commit.short_id) + end + + step 'project have an open merge request' do + create(:merge_request, + title: 'Feature', + source_project: @project, + source_branch: 'feature', + target_branch: 'master', + author: @project.users.first + ) + end + + step 'I click the "Compare" tab' do + click_link('Compare') + end + + step 'I fill compare fields with branches' do + fill_in 'from', with: 'master' + fill_in 'to', with: 'feature' + + click_button 'Compare' + end + + step 'I see compared branches' do + expect(page).to have_content 'Commits (1)' + expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions' + end + + step 'I see button to create a new merge request' do + expect(page).to have_link 'Create Merge Request' + end + + step 'I should not see button to create a new merge request' do + expect(page).to_not have_link 'Create Merge Request' + end + + step 'I should see button to the merge request' do + merge_request = MergeRequest.find_by(title: 'Feature') + expect(page).to have_link "View Open Merge Request", href: namespace_project_merge_request_path(@project.namespace, @project, merge_request) + end + step 'I see breadcrumb links' do expect(page).to have_selector('ul.breadcrumb') expect(page).to have_selector('ul.breadcrumb a', count: 4) diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 69695d493f3..8b9aa6aabfa 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -8,6 +8,15 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps visit namespace_project_issue_path(@project.namespace, @project, @issue) end + step 'I click the thumbsup award Emoji' do + page.within '.awards' do + thumbsup = page.find('.award .emoji-1F44D') + thumbsup.click + thumbsup.hover + sleep 0.3 + end + end + step 'I click to emoji-picker' do page.within '.awards-controls' do page.find('.add-award').click @@ -40,6 +49,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps page.within '.awards' do expect(page).to have_selector '.award' expect(page.find('.award.active .counter')).to have_content '1' + expect(page.find('.award.active')['data-original-title']).to eq('me') + end + end + + step 'I have no awards added' do + page.within '.awards' do + expect(page).to have_selector '.award' + expect(page.all('.award').size).to eq(2) + + # Check tooltip data + page.all('.award').each do |element| + expect(element['title']).to eq("") + end + + page.all('.award .counter').each do |element| + expect(element).to have_content '0' + end end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index d556b73f9fd..09a89e99831 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -54,6 +54,10 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps expect(page).to have_content "Release 0.4" end + step 'I should see issue "Tweet control"' do + expect(page).to have_content "Tweet control" + end + step 'I click link "New Issue"' do click_link "New Issue" end @@ -301,4 +305,5 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps def filter_issue(text) fill_in 'issue_search', with: text end + end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 92bf362879b..fa54c93df0f 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -6,8 +6,20 @@ module SharedBuilds end step 'project has a recent build' do - ci_commit = create :ci_commit, project: @project, sha: sample_commit.id - @build = create :ci_build, commit: ci_commit + @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha) + @build = create(:ci_build, commit: @ci_commit) + end + + step 'recent build is successful' do + @build.update_column(:status, 'success') + end + + step 'recent build failed' do + @build.update_column(:status, 'failed') + end + + step 'project has another build that is running' do + create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running') end step 'I visit recent build details page' do @@ -38,4 +50,21 @@ module SharedBuilds step 'I access artifacts download page' do visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build) end + + step 'I see details of a build' do + expect(page).to have_content "Build ##{@build.id}" + end + + step 'I see build trace' do + expect(page).to have_css '#build-trace' + end + + step 'I see the build' do + page.within('.commit_status') do + expect(page).to have_content "##{@build.id}" + expect(page).to have_content @build.sha[0..7] + expect(page).to have_content @build.ref + expect(page).to have_content @build.name + end + end end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 25c2b476f43..2117feaedb8 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -119,6 +119,24 @@ module SharedIssuable end end + step 'I should see "1 of 1" in the sidebar' do + expect_sidebar_content('1 of 1') + end + + step 'I should see "1 of 2" in the sidebar' do + expect_sidebar_content('1 of 2') + end + + step 'I should see "2 of 2" in the sidebar' do + expect_sidebar_content('2 of 2') + end + + step 'I click link "Next" in the sidebar' do + page.within '.issuable-sidebar' do + click_link 'Next' + end + end + def create_issuable_for_project(project_name:, title:, type: :issue) project = Project.find_by(name: project_name) @@ -159,4 +177,10 @@ module SharedIssuable expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") end + def expect_sidebar_content(content) + page.within '.issuable-sidebar' do + expect(page).to have_content content + end + end + end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index d9c75d12238..b13e82f276b 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -240,6 +240,18 @@ module SharedProject end end + step 'The project is internal' do + @project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + step 'public access for builds is enabled' do + @project.update(public_builds: true) + end + + step 'public access for builds is disabled' do + @project.update(public_builds: false) + end + def user_owns_project(user_name:, project_name:, visibility: :private) user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) project = Project.find_by(name: project_name) diff --git a/lib/api/builds.rb b/lib/api/builds.rb index d293f988165..a8bd3842ce4 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -13,11 +13,12 @@ module API # Example Request: # GET /projects/:id/builds get ':id/builds' do + builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Get builds for a specific commit of a project @@ -30,6 +31,8 @@ module API # Example Request: # GET /projects/:id/repository/commits/:sha/builds get ':id/repository/commits/:sha/builds' do + authorize_read_builds! + commit = user_project.ci_commits.find_by_sha(params[:sha]) return not_found! unless commit @@ -37,7 +40,7 @@ module API builds = filter_builds(builds, params[:scope]) present paginate(builds), with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Get a specific build of a project @@ -48,11 +51,13 @@ module API # Example Request: # GET /projects/:id/builds/:build_id get ':id/builds/:build_id' do + authorize_read_builds! + build = get_build(params[:build_id]) return not_found!(build) unless build present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Get a trace of a specific build of a project @@ -67,6 +72,8 @@ module API # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. get ':id/builds/:build_id/trace' do + authorize_read_builds! + build = get_build(params[:build_id]) return not_found!(build) unless build @@ -86,7 +93,7 @@ module API # example request: # post /projects/:id/build/:build_id/cancel post ':id/builds/:build_id/cancel' do - authorize_manage_builds! + authorize_update_builds! build = get_build(params[:build_id]) return not_found!(build) unless build @@ -94,7 +101,7 @@ module API build.cancel present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end # Retry a specific build of a project @@ -105,7 +112,7 @@ module API # example request: # post /projects/:id/build/:build_id/retry post ':id/builds/:build_id/retry' do - authorize_manage_builds! + authorize_update_builds! build = get_build(params[:build_id]) return forbidden!('Build is not retryable') unless build && build.retryable? @@ -113,7 +120,7 @@ module API build = Ci::Build.retry(build) present build, with: Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + user_can_download_artifacts: can?(current_user, :read_build, user_project) end end @@ -141,8 +148,12 @@ module API builds.where(status: available_statuses && scope) end - def authorize_manage_builds! - authorize! :manage_builds, user_project + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project end end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 1162271f5fc..9422d438d21 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -18,7 +18,7 @@ module API # Examples: # GET /projects/:id/repository/commits/:sha/statuses get ':id/repository/commits/:sha/statuses' do - authorize! :read_commit_statuses, user_project + authorize! :read_commit_status, user_project sha = params[:sha] ci_commit = user_project.ci_commit(sha) not_found! 'Commit' unless ci_commit diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 82a75734de0..a9c09ffdb31 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -72,6 +72,7 @@ module API expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } + expose :public_builds end class ProjectMember < UserBasic @@ -175,6 +176,7 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds + expose :merge_status end class MergeRequestChanges < MergeRequest @@ -383,7 +385,7 @@ module API # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255) expose :download_url do |repo_obj, options| if options[:user_can_download_artifacts] - repo_obj.download_url + repo_obj.artifacts_download_url end end expose :commit, with: RepoCommit do |repo_obj, _options| diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index dd7f24f3279..c5e5d57ed4d 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -71,6 +71,7 @@ module API # title (required) - Title of MR # description - Description of MR # labels (optional) - Labels for MR as a comma-separated list + # milestone_id (optional) - Milestone ID # # Example: # POST /projects/:id/merge_requests @@ -78,7 +79,7 @@ module API post ":id/merge_requests" do authorize! :create_merge_request, user_project required_attributes! [:source_branch, :target_branch, :title] - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id] # Validate label names in advance if (errors = validate_label_params(params)).any? @@ -163,11 +164,12 @@ module API # state_event - Status of MR. (close|reopen|merge) # description - Description of MR # labels (optional) - Labels for a MR as a comma-separated list + # milestone_id (optional) - Milestone ID # Example: # PUT /projects/:id/merge_requests/:merge_request_id # put path do - attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description] + attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :update_merge_request, merge_request @@ -300,6 +302,19 @@ module API render_api_error!("Failed to save note #{note.errors.messages}", 400) end end + + # List issues that will close on merge + # + # Parameters: + # id (required) - The ID of a project + # merge_request_id (required) - ID of MR + # Examples: + # GET /projects/:id/merge_requests/:merge_request_id/closes_issues + get "#{path}/closes_issues" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) + present paginate(issues), with: Entities::Issue + end end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 1f991e600e3..6067c8b4a5e 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -99,6 +99,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - 0 by default # import_url (optional) + # public_builds (optional) # Example Request # POST /projects post do @@ -115,7 +116,8 @@ module API :namespace_id, :public, :visibility_level, - :import_url] + :import_url, + :public_builds] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -145,6 +147,7 @@ module API # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) # import_url (optional) + # public_builds (optional) # Example Request # POST /projects/user/:user_id post "user/:user_id" do @@ -161,7 +164,8 @@ module API :shared_runners_enabled, :public, :visibility_level, - :import_url] + :import_url, + :public_builds] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -205,6 +209,7 @@ module API # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project + # public_builds (optional) # Example Request # PUT /projects/:id put ':id' do @@ -219,7 +224,8 @@ module API :snippets_enabled, :shared_runners_enabled, :public, - :visibility_level] + :visibility_level, + :public_builds] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 5e4964f446c..d1d07394e92 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -54,7 +54,7 @@ module API # GET /projects/:id/triggers get ':id/triggers' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project triggers = user_project.triggers.includes(:trigger_requests) triggers = paginate(triggers) @@ -71,7 +71,7 @@ module API # GET /projects/:id/triggers/:token get ':id/triggers/:token' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.find_by(token: params[:token].to_s) return not_found!('Trigger') unless trigger @@ -87,7 +87,7 @@ module API # POST /projects/:id/triggers post ':id/triggers' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.create @@ -103,7 +103,7 @@ module API # DELETE /projects/:id/triggers/:token delete ':id/triggers/:token' do authenticate! - authorize_admin_project + authorize! :admin_build, user_project trigger = user_project.triggers.find_by(token: params[:token].to_s) return not_found!('Trigger') unless trigger diff --git a/lib/api/variables.rb b/lib/api/variables.rb index d9a055f6c92..f6495071a11 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -2,7 +2,7 @@ module API # Projects variables API class Variables < Grape::API before { authenticate! } - before { authorize_admin_project } + before { authorize! :admin_build, user_project } resource :projects do # Get project variables diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 099062eeb8b..4962f5e53ce 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,9 @@ module Backup class Manager def pack + # Make sure there is a connection + ActiveRecord::Base.connection.reconnect! + # saving additional informations s = {} s[:db_version] = "#{ActiveRecord::Migrator.current_version}" diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb deleted file mode 100644 index f1331c0ebf9..00000000000 --- a/lib/banzai/pipeline/asciidoc_pipeline.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Banzai - module Pipeline - class AsciidocPipeline < BasePipeline - def self.filters - [ - Filter::RelativeLinkFilter - ] - end - end - end -end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb new file mode 100644 index 00000000000..4bb85e24c38 --- /dev/null +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -0,0 +1,16 @@ +module Banzai + module Pipeline + class BroadcastMessagePipeline < DescriptionPipeline + def self.filters + @filters ||= [ + Filter::MarkdownFilter, + Filter::SanitizationFilter, + + Filter::EmojiFilter, + Filter::AutolinkFilter, + Filter::ExternalLinkFilter + ] + end + end + end +end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index b203b9d70e4..0b9c2e730f9 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -31,9 +31,7 @@ module Gitlab html = ::Asciidoctor.convert(input, asciidoc_opts) - if context[:project] - html = Banzai.render(html, context.merge(pipeline: :asciidoc)) - end + html = Banzai.post_process(html, context) html.html_safe end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index f751458ac66..b9bb6e76081 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -36,7 +36,7 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240']) + output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '900']) raise Error, output unless status.zero? true end diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb index 313e6b9fc03..997a22779a0 100644 --- a/lib/gitlab/blame.rb +++ b/lib/gitlab/blame.rb @@ -40,6 +40,7 @@ module Gitlab end def highlighted_lines + @blob.load_all_data!(repository) @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.name, @blob.data).lines end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 6ebb8027553..6f9da69983a 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -17,7 +17,7 @@ module Gitlab end def true_value - if self.class.postgresql? + if Gitlab::Database.postgresql? "'t'" else 1 @@ -25,7 +25,7 @@ module Gitlab end def false_value - if self.class.postgresql? + if Gitlab::Database.postgresql? "'f'" else 0 @@ -47,9 +47,5 @@ module Gitlab row.first end end - - def connection - self.class.connection - end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index f065cc5e9e9..191bea86ac3 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -1,8 +1,8 @@ module Gitlab module Git - BLANK_SHA = '0' * 40 - TAG_REF_PREFIX = "refs/tags/" - BRANCH_REF_PREFIX = "refs/heads/" + BLANK_SHA = ('0' * 40).freeze + TAG_REF_PREFIX = "refs/tags/".freeze + BRANCH_REF_PREFIX = "refs/heads/".freeze class << self def ref_name(ref) diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index ea6b0ee796d..71cf6a0d886 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -53,13 +53,10 @@ module Gitlab object_kind: "note", user: user.hook_attrs, project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url, - }, - object_attributes: note.hook_attrs + project: project.hook_attrs, + object_attributes: note.hook_attrs, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } base_data[:object_attributes][:url] = diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb new file mode 100644 index 00000000000..746ec283330 --- /dev/null +++ b/lib/gitlab/other_markup.rb @@ -0,0 +1,24 @@ +module Gitlab + # Parser/renderer for markups without other special support code. + module OtherMarkup + + # Public: Converts the provided markup into HTML. + # + # input - the source text in a markup format + # context - a Hash with the template context: + # :commit + # :project + # :project_wiki + # :requested_path + # :ref + # + def self.render(file_name, input, context) + html = GitHub::Markup.render(file_name, input). + force_encoding(input.encoding) + + html = Banzai.post_process(html, context) + + html.html_safe + end + end +end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 4f9cdef3869..1dad621aa00 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -47,18 +47,14 @@ module Gitlab user_id: user.id, user_name: user.name, user_email: user.email, + user_avatar: user.avatar_url, project_id: project.id, - repository: { - name: project.name, - url: project.url_to_repo, - description: project.description, - homepage: project.web_url, - git_http_url: project.http_url_to_repo, - git_ssh_url: project.ssh_url_to_repo, - visibility_level: project.visibility_level - }, + project: project.hook_attrs, commits: commit_attrs, - total_commits_count: commits_count + total_commits_count: commits_count, + # DEPRECATED + repository: project.hook_attrs.slice(:name, :url, :description, :homepage, + :git_http_url, :git_ssh_url, :visibility_level) } data diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 38364a0b151..addda95be2b 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -12,9 +12,9 @@ module Gitlab def objects(scope, page = nil) case scope when 'snippet_titles' - Kaminari.paginate_array(snippet_titles).page(page).per(per_page) + snippet_titles.page(page).per(per_page) when 'snippet_blobs' - Kaminari.paginate_array(snippet_blobs).page(page).per(per_page) + snippet_blobs.page(page).per(per_page) else super end @@ -39,11 +39,7 @@ module Gitlab end def snippet_blobs - search = Snippet.where(id: limit_snippet_ids).search_code(query) - search = search.order('updated_at DESC').to_a - snippets = [] - search.each { |e| snippets << chunk_snippet(e) } - snippets + Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC') end def default_scope diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 4e6e56ac2db..cc8617b72ca 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -34,11 +34,16 @@ sidekiq_pid_path="$pid_path/sidekiq.pid" # /home/git/gitlab-workhorse . gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" + # The -listenXxx settings determine where gitlab-workhorse -# listens for connections from NGINX. To listen on localhost:8181, write -# '-listenNetwork tcp -listenAddr localhost:8181'. -# The -authBackend setting tells gitlab-workhorse where it can reach -# Unicorn. +# listens for connections from the web server. By default it listens to a +# socket. To listen on TCP connections (needed by Apache) change to: +# '-listenNetwork tcp -listenAddr 127.0.0.1:8181' +# +# The -authBackend setting tells gitlab-workhorse where it can reach Unicorn. +# For relative URL support change to: +# '-authBackend http://127.0.0.1/8080/gitlab' +# Read more in http://doc.gitlab.com/ce/install/relative_url.html gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2dc2953e328..54d95cd62a5 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -90,24 +90,6 @@ namespace :gitlab do end end - def check_database_is_not_sqlite - print "Database is SQLite ... " - - database_config_file = Rails.root.join("config", "database.yml") - - unless File.read(database_config_file) =~ /adapter:\s+sqlite/ - puts "no".green - else - puts "yes".red - puts "Please fix this by removing the SQLite entry from the database.yml".blue - for_more_information( - "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", - see_database_guide - ) - fix_and_rerun - end - end - def check_gitlab_config_exists print "GitLab config exists? ... " diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 5987988dc8e..b6f076a90c3 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,15 +1,15 @@ #!/bin/bash if [ -f /.dockerinit ]; then - # Docker runners use `/cache` folder which is persisted every build - if [ ! -e /cache/phantomjs_1.9.8-0jessie_amd64.deb ]; then + mkdir -p vendor + if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - mv phantomjs_1.9.8-0jessie_amd64.deb /cache + mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ fi - dpkg -i /cache/phantomjs_1.9.8-0jessie_amd64.deb + dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb apt-get update -qq - apt-get -o dir::cache::archives="/cache/apt" install -y -qq --force-yes \ + apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip cp config/database.yml.mysql config/database.yml @@ -20,7 +20,7 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - export FLAGS=(--path /cache) + export FLAGS=(--path vendor) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb new file mode 100644 index 00000000000..438e776ec4b --- /dev/null +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Projects::CommitController do + describe 'GET show' do + let(:project) { create(:project) } + + before do + user = create(:user) + project.team << [user, :master] + + sign_in(user) + end + + context 'with valid id' do + it 'responds with 200' do + go id: project.commit.id + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with 404' do + go id: project.commit.id.reverse + + expect(response).to be_not_found + end + end + + def go(id:) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index d2db77f6286..c1b6ecd329a 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -1,30 +1,3 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_build, class: Ci::Build do name 'test' @@ -65,5 +38,20 @@ FactoryGirl.define do build.trace = 'BUILD TRACE' end end + + trait :artifacts do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root + + 'spec/fixtures/ci_build_artifacts.zip', + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root + + 'spec/fixtures/ci_build_artifacts_metadata.gz', + 'application/x-gzip') + build.save! + end + end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 5b6f3cb3f15..6da3a857b3f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -8,7 +8,7 @@ describe "Builds" do @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit @project = @commit.project - @project.team << [@user, :master] + @project.team << [@user, :developer] end describe "GET /:project/builds" do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 5a62da10619..dacaa96d760 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -8,7 +8,6 @@ describe 'Commits' do describe 'CI' do before do login_as :user - project.team << [@user, :master] stub_ci_commit_to_return_yaml_file end @@ -19,6 +18,10 @@ describe 'Commits' do context 'commit status is Generic Commit Status' do let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } + before do + project.team << [@user, :reporter] + end + describe 'Commit builds' do before do visit ci_status_path(commit) @@ -37,85 +40,126 @@ describe 'Commits' do context 'commit status is Ci Build' do let!(:build) { FactoryGirl.create :ci_build, commit: commit } + let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - describe 'Project commits' do + context 'when logged as developer' do before do - visit namespace_project_commits_path(project.namespace, project, :master) + project.team << [@user, :developer] end - it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_css(".ci-status-link") + describe 'Project commits' do + before do + visit namespace_project_commits_path(project.namespace, project, :master) end - end - end - describe 'Commit builds' do - before do - visit ci_status_path(commit) + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") + end + end end - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } - end - - context 'Download artifacts' do - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - - before do - build.update_attributes(artifacts_file: artifacts_file) - end + describe 'Commit builds' do + before do + visit ci_status_path(commit) + end - it do - visit ci_status_path(commit) - click_on 'Download artifacts' - expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content commit.git_commit_message } + it { expect(page).to have_content commit.git_author_name } end - end - describe 'Cancel all builds' do - it 'cancels commit' do - visit ci_status_path(commit) - click_on 'Cancel running' - expect(page).to have_content 'canceled' - end - end + context 'Download artifacts' do + before do + build.update_attributes(artifacts_file: artifacts_file) + end - describe 'Cancel build' do - it 'cancels build' do - visit ci_status_path(commit) - click_on 'Cancel' - expect(page).to have_content 'canceled' + it do + visit ci_status_path(commit) + click_on 'Download artifacts' + expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + end end - end - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do + describe 'Cancel all builds' do + it 'cancels commit' do visit ci_status_path(commit) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + click_on 'Cancel running' + expect(page).to have_content 'canceled' end + end - it 'shows warning' do - stub_ci_commit_yaml_file(nil) + describe 'Cancel build' do + it 'cancels build' do visit ci_status_path(commit) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' + click_on 'Cancel' + expect(page).to have_content 'canceled' end end - context 'ci builds disabled' do - before do - stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) + describe '.gitlab-ci.yml not found warning' do + context 'ci builds enabled' do + it "does not show warning" do + visit ci_status_path(commit) + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end end - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + context 'ci builds disabled' do + before do + stub_ci_builds_disabled + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + end + + it 'does not show warning' do + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end end end end + + context "when logged as reporter" do + before do + project.team << [@user, :reporter] + build.update_attributes(artifacts_file: artifacts_file) + visit ci_status_path(commit) + end + + it do + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') + end + end + + context 'when accessing internal project with disallowed access' do + before do + project.update( + visibility_level: Gitlab::VisibilityLevel::INTERNAL, + public_builds: false) + build.update_attributes(artifacts_file: artifacts_file) + visit ci_status_path(commit) + end + + it do + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to_not have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') + end + end end end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 655d2c8b7d9..b98476f854e 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -96,6 +96,60 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for :visitor } end + describe "GET /:project_path/builds" do + subject { namespace_project_builds_path(project.namespace, project) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + + describe "GET /:project_path/builds/:id" do + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit) } + subject { namespace_project_build_path(project.namespace, project, build.id) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } + end + + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :visitor } + end + end + describe "GET /:project_path/blob" do before do commit = project.repository.commit diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 30e353148a8..f6c1005d265 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -293,6 +293,10 @@ describe ApplicationHelper do describe 'render_markup' do let(:content) { 'Noël' } + let(:user) { create(:user) } + before do + allow(helper).to receive(:current_user).and_return(user) + end it 'should preserve encoding' do expect(content.encoding.name).to eq('UTF-8') diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 6beb21c6d2b..736bf787208 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -42,22 +42,6 @@ module Gitlab end end - context "with project in context" do - - let(:context) { { project: create(:project) } } - - it "should filter converted input via HTML pipeline and return result" do - filtered_html = '<b>ASCII</b>' - - 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 bd8688fefa1..d0a447753b7 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 691f36e6cb7..da652677443 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 3ef61685398..257e4a38435 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 00000000000..0df89938e97 --- /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 00000000000..5b575da34f3 --- /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 7289e596ef3..232a11245a6 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', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>" 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 00000000000..48c851ebbd6 --- /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 f4c58882757..b1764d7ac09 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 021d62cdf0c..600089802b2 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 00000000000..a20a6149649 --- /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 2aedca20df2..2d8f1cc1ad3 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 c61ddf01118..f35b48601ad 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 30a71987d86..1b1380ce4e2 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 c484ae8fc8c..e1ee43e64db 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 cee051f5732..47ce409fe4b 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 8c9f5a382b7..6c07802db8b 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 21482fc1070..89b554622ef 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 d7bfa17b0b1..4fd1df25568 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 244947762dd..01b369720ca 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 c1080ef190a..eb3a5fe43f5 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 d3364a71022..1bdc03af12d 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 00000000000..b924a208e71 --- /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 00000000000..422083875d7 --- /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 |