diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2d33bad5886..2eda2a6007d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,6 +28,7 @@ stages: - prepare - test - post-test +- pages # Prepare and merge knapsack tests .knapsack-state: &knapsack-state @@ -40,6 +41,7 @@ stages: paths: - knapsack/ artifacts: + expire_in: 31d paths: - knapsack/ @@ -81,8 +83,10 @@ update-knapsack: - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} - knapsack rspec artifacts: + expire_in: 31d paths: - knapsack/ + - coverage/ .spinach-knapsack: &spinach-knapsack stage: test @@ -97,8 +101,10 @@ update-knapsack: - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: + expire_in: 31d paths: - knapsack/ + - coverage/ rspec 0 20: *rspec-knapsack rspec 1 20: *rspec-knapsack @@ -186,14 +192,14 @@ spinach 9 10 ruby23: *spinach-knapsack-ruby23 # Other generic tests -.static-analyses-variables: &static-analyses-variables +.ruby-static-analysis: &ruby-static-analysis variables: SIMPLECOV: "false" USE_DB: "false" USE_BUNDLE_INSTALL: "true" .exec: &exec - <<: *static-analyses-variables + <<: *ruby-static-analysis stage: test script: - bundle exec $CI_BUILD_NAME @@ -220,12 +226,28 @@ teaspoon: bundler:audit: stage: test - <<: *static-analyses-variables + <<: *ruby-static-analysis only: - master script: - "bundle exec bundle-audit check --update --ignore OSVDB-115941" +coverage: + stage: post-test + services: [] + variables: + USE_DB: "false" + USE_BUNDLE_INSTALL: "true" + script: + - bundle exec scripts/merge-simplecov + artifacts: + name: coverage + expire_in: 31d + paths: + - coverage/index.html + - coverage/assets/ + + # Notify slack in the end notify:slack: @@ -238,3 +260,18 @@ notify:slack: - tags@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee - tags@gitlab-org/gitlab-ee + +pages: + before_script: [] + stage: pages + dependencies: + - coverage + script: + - mv public/ .public/ + - mkdir public/ + - mv coverage public/coverage-ruby + artifacts: + paths: + - public + only: + - master diff --git a/.simplecov b/.simplecov deleted file mode 100644 index d979288df44..00000000000 --- a/.simplecov +++ /dev/null @@ -1,4 +0,0 @@ -# .simplecov -SimpleCov.start 'rails' do - merge_timeout 3600 -end diff --git a/CHANGELOG b/CHANGELOG index 181829a86a5..2704f89ccda 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,27 +1,57 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.11.0 (unreleased) + - Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Fix CI status icon link underline (ClemMakesApps) - Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable + - Optimize maximum user access level lookup in loading of notes - Limit git rev-list output count to one in forced push check + - Clean up unused routes (Josef Strzibny) - Add green outline to New Branch button. !5447 (winniehell) - Retrieve rendered HTML from cache in one request + - Fix renaming repository when name contains invalid chararacters under project settings - Nokogiri's various parsing methods are now instrumented + - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 + - Add build event color in HipChat messages (David Eisner) - Make fork counter always clickable. !5463 (winniehell) + - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333 + - The overhead of instrumented method calls has been reduced - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Load project invited groups and members eagerly in `ProjectTeam#fetch_members` + - Make branches sortable without push permission !5462 (winniehell) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem + - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) + - Profile requests when a header is passed + - Add commit stats in commit api. !5517 (dixpac) + - Make error pages responsive (Takuya Noguchi) + - Change requests_profiles resource constraint to catch virtually any file + - Reduce number of queries made for merge_requests/:id/diffs -v 8.10.2 (unreleased) +v 8.10.3 (unreleased) + +v 8.10.2 - User can now search branches by name. !5144 + - Page is now properly rendered after committing the first file and creating the first branch. !5399 + - Add branch or tag icon to ref in builds page. !5434 - Fix backup restore. !5459 - - Disable MySQL foreign key checks before dropping all tables. !5472 - Use project ID in repository cache to prevent stale data from persisting across projects. !5460 + - Fix issue with autocomplete search not working with enter key. !5466 + - Add iid to MR API response. !5468 + - Disable MySQL foreign key checks before dropping all tables. !5472 - Ensure relative paths for video are rewritten as we do for images. !5474 - Ensure current user can retry a build before showing the 'Retry' button. !5476 + - Add ENV variable to skip repository storages validations. !5478 + - Added `*.js.es6 gitlab-language=javascript` to `.gitattributes`. !5486 + - Don't show comment button in gutter of diffs on MR discussion tab. !5493 + - Rescue Rugged::OSError (lock exists) when creating references. !5497 + - Fix expand all diffs button in compare view. !5500 + - Show release notes in tags list. !5503 + - Fix a bug where forking a project from a repository storage to another would fail. !5509 + - Fix missing schema update for `20160722221922`. !5512 + - Update `gitlab-shell` version to 3.2.1 in the 8.9->8.10 update guide. !5516 v 8.10.1 - Refactor repository storages documentation. !5428 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 944880fa15e..e4604e3afd0 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/Gemfile b/Gemfile index f2ac74a5976..071277de068 100644 --- a/Gemfile +++ b/Gemfile @@ -225,7 +225,7 @@ gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' gem 'font-awesome-rails', '~> 4.6.1' gem 'gemojione', '~> 3.0' -gem 'gon', '~> 6.0.1' +gem 'gon', '~> 6.1.0' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.1.0' gem 'jquery-ui-rails', '~> 5.0.0' @@ -253,7 +253,7 @@ group :development do gem 'letter_opener_web', '~> 1.3.0' gem 'rerun', '~> 0.11.0' - gem 'bullet', '~> 5.0.0', require: false + gem 'bullet', '~> 5.2.0', require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'web-console', '~> 2.0' @@ -275,7 +275,7 @@ group :development, :test do gem 'awesome_print', '~> 1.2.0', require: false gem 'fuubar', '~> 2.0.0' - gem 'database_cleaner', '~> 1.4.0' + gem 'database_cleaner', '~> 1.5.0' gem 'factory_girl_rails', '~> 4.6.0' gem 'rspec-rails', '~> 3.5.0' gem 'rspec-retry', '~> 0.4.5' @@ -303,7 +303,7 @@ group :development, :test do gem 'rubocop', '~> 0.41.2', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false - gem 'simplecov', '~> 0.11.0', require: false + gem 'simplecov', '0.12.0', require: false gem 'flog', '~> 4.3.2', require: false gem 'flay', '~> 2.6.1', require: false gem 'bundler-audit', '~> 0.5.0', require: false @@ -334,6 +334,8 @@ gem 'mail_room', '~> 0.8' gem 'email_reply_parser', '~> 0.5.8' +gem 'ruby-prof', '~> 0.15.9' + ## CI gem 'activerecord-session_store', '~> 1.0.0' gem 'nested_form', '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index bfa7e38da85..670578dec6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -59,7 +59,7 @@ GEM oauth2 (~> 1.0) asciidoctor (1.5.3) ast (2.3.0) - attr_encrypted (3.0.1) + attr_encrypted (3.0.3) encryptor (~> 3.0.0) attr_required (1.0.0) autoprefixer-rails (6.2.3) @@ -104,9 +104,9 @@ GEM brakeman (3.3.2) browser (2.2.0) builder (3.2.2) - bullet (5.0.0) + bullet (5.2.0) activesupport (>= 3.0.0) - uniform_notifier (~> 1.9.0) + uniform_notifier (~> 1.10.0) bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) @@ -153,11 +153,11 @@ GEM d3_rails (3.5.11) railties (>= 3.1.0) daemons (1.2.3) - database_cleaner (1.4.1) + database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) - default_value_for (3.0.1) - activerecord (>= 3.2.0, < 5.0) + default_value_for (3.0.2) + activerecord (>= 3.2.0, < 5.1) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) devise (4.1.1) @@ -303,7 +303,7 @@ GEM gollum-rugged_adapter (0.4.2) mime-types (>= 1.15) rugged (~> 0.24.0, >= 0.21.3) - gon (6.0.1) + gon (6.1.0) actionpack (>= 3.0) json multi_json @@ -509,7 +509,7 @@ GEM rack-cors (0.4.0) rack-mount (0.8.3) rack (>= 1.0.0) - rack-oauth2 (1.2.1) + rack-oauth2 (1.2.3) activesupport (>= 2.3) attr_required (>= 0.0.5) httpclient (>= 2.4) @@ -575,7 +575,7 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.7) redis (>= 2.2) - request_store (1.3.0) + request_store (1.3.1) rerun (0.11.0) listen (~> 3.0) responders (2.1.1) @@ -620,6 +620,7 @@ GEM rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) + ruby-prof (0.15.9) ruby-progressbar (1.8.1) ruby-saml (1.3.0) nokogiri (>= 1.5.10) @@ -672,9 +673,9 @@ GEM rufus-scheduler (>= 2.0.24) sidekiq (>= 4.0.0) simple_oauth (0.1.9) - simplecov (0.11.2) + simplecov (0.12.0) docile (~> 1.1.0) - json (~> 1.8) + json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) sinatra (1.4.7) @@ -774,7 +775,7 @@ GEM unicorn-worker-killer (0.4.4) get_process_mem (~> 0) unicorn (>= 4, < 6) - uniform_notifier (1.9.0) + uniform_notifier (1.10.0) uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) @@ -829,7 +830,7 @@ DEPENDENCIES bootstrap-sass (~> 3.3.0) brakeman (~> 3.3.0) browser (~> 2.2) - bullet (~> 5.0.0) + bullet (~> 5.2.0) bundler-audit (~> 0.5.0) byebug (~> 8.2.1) capybara (~> 2.6.2) @@ -841,7 +842,7 @@ DEPENDENCIES connection_pool (~> 2.0) creole (~> 0.5.0) d3_rails (~> 3.5.0) - database_cleaner (~> 1.4.0) + database_cleaner (~> 1.5.0) default_value_for (~> 3.0.0) devise (~> 4.0) devise-two-factor (~> 3.0.0) @@ -874,7 +875,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) - gon (~> 6.0.1) + gon (~> 6.1.0) grape (~> 0.13.0) grape-entity (~> 0.4.2) hamlit (~> 2.5) @@ -948,6 +949,7 @@ DEPENDENCIES rubocop (~> 0.41.2) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) + ruby-prof (~> 0.15.9) sanitize (~> 2.0) sass-rails (~> 5.0.0) scss_lint (~> 0.47.0) @@ -960,7 +962,7 @@ DEPENDENCIES shoulda-matchers (~> 2.8.0) sidekiq (~> 4.0) sidekiq-cron (~> 0.4.0) - simplecov (~> 0.11.0) + simplecov (= 0.12.0) sinatra (~> 1.4.4) six (~> 0.2.0) slack-notifier (~> 1.2.0) diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 55b6f132bab..0f840821f53 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -66,4 +66,12 @@ })(); + $(function() { + if ($('.js-importer-status').length) { + var jobsImportPath = $('.js-importer-status').data('jobs-import-path'); + var importPath = $('.js-importer-status').data('import-path'); + + new ImporterStatus(jobsImportPath, importPath); + } + }); }).call(this); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 64a29d36cdf..4af2a214e12 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -189,6 +189,7 @@ _this.groupId = $(select).data('group-id'); _this.showCurrentUser = $(select).data('current-user'); _this.authorId = $(select).data('author-id'); + _this.skipUsers = $(select).data('skip-users'); showNullUser = $(select).data('null-user'); showAnyUser = $(select).data('any-user'); showEmailUser = $(select).data('email-user'); @@ -320,7 +321,8 @@ project_id: this.projectId, group_id: this.groupId, current_user: this.showCurrentUser, - author_id: this.authorId + author_id: this.authorId, + skip_users: this.skipUsers }, dataType: "json" }).done(function(users) { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index ee3b2d2b801..dfe1e3075da 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -99,3 +99,33 @@ form.edit-issue { .issue-form .select2-container { width: 250px !important; } + +.issues-footer { + padding-top: $gl-padding; + padding-bottom: 37px; +} + +.issue-email-modal-btn { + padding: 0; + color: $gl-link-color; + background-color: transparent; + border: 0; + outline: 0; + + &:hover { + text-decoration: underline; + } +} + +.email-modal-input-group { + margin-bottom: 10px; + + .form-control { + background-color: $white-light; + } + + .btn { + background-color: $background-color; + border: 1px solid $border-gray-light; + } +} diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb new file mode 100644 index 00000000000..a478176e138 --- /dev/null +++ b/app/controllers/admin/requests_profiles_controller.rb @@ -0,0 +1,17 @@ +class Admin::RequestsProfilesController < Admin::ApplicationController + def index + @profile_token = Gitlab::RequestProfiler.profile_token + @profiles = Gitlab::RequestProfiler::Profile.all.group_by(&:request_path) + end + + def show + clean_name = Rack::Utils.clean_path_info(params[:name]) + profile = Gitlab::RequestProfiler::Profile.find(clean_name) + + if profile + render text: profile.content + else + redirect_to admin_requests_profiles_path, alert: 'Profile not found' + end + end +end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index c89678cf2d8..d828d163c28 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -5,6 +5,7 @@ class AutocompleteController < ApplicationController def users @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? + @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present? @users = @users.active @users = @users.reorder(:name) @users = @users.page(params[:page]) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index 461fc059a3c..a1ab8b99048 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,5 +1,5 @@ class Explore::ApplicationController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! layout 'explore' end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index f7b44099b78..4eca278599f 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,5 +1,5 @@ class HelpController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! layout 'help' diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 6126acccaab..e926043f3eb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -6,6 +6,7 @@ class Projects::BranchesController < Projects::ApplicationController before_action :authorize_push_code!, only: [:new, :create, :destroy] def index + @sort = params[:sort].presence || 'name' @branches = BranchesFinder.new(@repository, params).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index fa663c9bda4..3c6f29ac0ba 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,4 +1,5 @@ class Projects::IssuesController < Projects::ApplicationController + include NotesHelper include ToggleSubscriptionAction include IssuableActions include ToggleAwardEmoji @@ -70,6 +71,8 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @noteable = @issue + preload_max_access_for_authors(@notes, @project) + respond_to do |format| format.html format.json do @@ -79,7 +82,7 @@ class Projects::IssuesController < Projects::ApplicationController end def create - @issue = Issues::CreateService.new(project, current_user, issue_params).execute + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute respond_to do |format| format.html do @@ -89,7 +92,7 @@ class Projects::IssuesController < Projects::ApplicationController render :new end end - format.js do |format| + format.js do @link = @issue.attachment.url.to_js end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 72ab7cff90c..b162384d44d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -3,6 +3,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include DiffForPath include DiffHelper include IssuableActions + include NotesHelper include ToggleAwardEmoji before_action :module_enabled @@ -382,6 +383,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @project_wiki, @ref ) + + preload_max_access_for_authors(@notes, @project) end def define_widget_vars @@ -401,7 +404,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController } @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? - @grouped_diff_discussions = @merge_request.notes.grouped_diff_discussions + @grouped_diff_discussions = @merge_request.notes.inc_author_project_award_emoji.grouped_diff_discussions Banzai::NoteRenderer.render( @grouped_diff_discussions.values.flat_map(&:notes), diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 6dc495247c8..8592579abbd 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -10,11 +10,12 @@ class Projects::TagsController < Projects::ApplicationController @tags = @repository.tags_sorted_by(@sort) @tags = Kaminari.paginate_array(@tags).page(params[:page]) - @releases = project.releases.where(tag: @tags) + @releases = project.releases.where(tag: @tags.map(&:name)) end def show @tag = @repository.find_tag(params[:id]) + @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.target) end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 69c92d2bed2..61517d21f9f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - skip_before_action :authenticate_user!, :reject_blocked + skip_before_action :authenticate_user!, :reject_blocked! include SearchHelper diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 87bf84adad1..9d9b0dba57f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -7,7 +7,7 @@ module NotesHelper end def note_editable?(note) - note.editable? && can?(current_user, :admin_note, note) + Ability.can_edit_note?(current_user, note) end def noteable_json(noteable) @@ -85,14 +85,13 @@ module NotesHelper data: data, title: 'Add a reply' end - def note_max_access_for_user(note) - @max_access_by_user_id ||= Hash.new do |hash, key| - project = key[:project] - hash[key] = project.team.human_max_access(key[:user_id]) - end + def preload_max_access_for_authors(notes, project) + user_ids = notes.map(&:author_id) + project.team.max_member_access_for_user_ids(user_ids) + end - full_key = { project: note.project, user_id: note.author_id } - @max_access_by_user_id[full_key] + def note_max_access_for_user(note) + note.project.team.human_max_access(note.author_id) end def discussion_diff_path(discussion) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index bb395e37884..5f27e33c6ad 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -5,21 +5,9 @@ module SelectsHelper css_class << "skip_ldap " if opts[:skip_ldap] css_class << (opts[:class] || '') value = opts[:selected] || '' - - first_user = opts[:first_user] && current_user ? current_user.username : false - html = { class: css_class, - data: { - placeholder: opts[:placeholder] || 'Search for a user', - null_user: opts[:null_user] || false, - any_user: opts[:any_user] || false, - email_user: opts[:email_user] || false, - first_user: first_user, - current_user: opts[:current_user] || false, - "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' - } + data: users_select_data_attributes(opts) } unless opts[:scope] == :all @@ -68,4 +56,20 @@ module SelectsHelper hidden_field_tag(id, value, class: css_class) end + + private + + def users_select_data_attributes(opts) + { + placeholder: opts[:placeholder] || 'Search for a user', + null_user: opts[:null_user] || false, + any_user: opts[:any_user] || false, + email_user: opts[:email_user] || false, + first_user: opts[:first_user] && current_user ? current_user.username : false, + current_user: opts[:current_user] || false, + "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], + author_id: opts[:author_id] || '', + skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil, + } + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index ac3bf441c32..5075076c27b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -389,6 +389,18 @@ class Ability GroupProjectsFinder.new(group).execute(user).any? end + def can_edit_note?(user, note) + return false if !note.editable? || !user.present? + return true if note.author == user || user.admin? + + if note.project + max_access_level = note.project.team.max_member_access(user.id) + max_access_level >= Gitlab::Access::MASTER + else + false + end + end + def namespace_abilities(user, namespace) rules = [] diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cbfa14e81f1..aac78d75f57 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -331,7 +331,7 @@ module Ci end def valid_token?(token) - project.valid_runners_token? token + project.valid_runners_token?(token) end def has_tags? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index acb6f5a2998..cbae1cd439b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -17,7 +17,7 @@ module Issuable belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :milestone - has_many :notes, as: :noteable, dependent: :destroy do + has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do def authors_loaded? # We check first if we're loaded to not load unnecessarily. loaded? && to_a.all? { |note| note.association(:author).loaded? } diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb new file mode 100644 index 00000000000..3b8e6df2da9 --- /dev/null +++ b/app/models/concerns/spammable.rb @@ -0,0 +1,16 @@ +module Spammable + extend ActiveSupport::Concern + + included do + attr_accessor :spam + after_validation :check_for_spam, on: :create + end + + def spam? + @spam + end + + def check_for_spam + self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 60af8c15340..d9428ebc9fb 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -6,6 +6,7 @@ class Issue < ActiveRecord::Base include Referable include Sortable include Taskable + include Spammable DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 7bff9f4f052..bbb9f2346eb 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -25,6 +25,14 @@ class LegacyDiffNote < Note @discussion_id ||= Digest::SHA1.hexdigest(self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)) end + def project_repository + if RequestStore.active? + RequestStore.fetch("project:#{project_id}:repository") { self.project.repository } + else + self.project.repository + end + end + def diff_file_hash line_code.split('_')[0] if line_code end @@ -34,7 +42,7 @@ class LegacyDiffNote < Note end def diff_file - @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff + @diff_file ||= Gitlab::Diff::File.new(diff, repository: project_repository) if diff end def diff_line diff --git a/app/models/member.rb b/app/models/member.rb index 44db3d977fa..24ab1276ee9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -53,6 +53,10 @@ class Member < ActiveRecord::Base default_value_for :notification_level, NotificationSetting.levels[:global] class << self + def access_for_user_ids(user_ids) + where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h + end + def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) find_by(invite_token: invite_token) diff --git a/app/models/project.rb b/app/models/project.rb index 023b1dc3725..dc44a757b4b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -451,7 +451,9 @@ class Project < ActiveRecord::Base def add_import_job if forked? - job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, + self.namespace.path) else job_id = RepositoryImportWorker.perform_async(self.id) end @@ -584,7 +586,11 @@ class Project < ActiveRecord::Base end def to_param - path + if persisted? && errors.include?(:path) + path_was + else + path + end end def to_reference(_from_project = nil) @@ -599,6 +605,13 @@ class Project < ActiveRecord::Base web_url.split('://')[1] end + def new_issue_address(author) + if Gitlab::IncomingEmail.enabled? && author + Gitlab::IncomingEmail.reply_address( + "#{path_with_namespace}+#{author.authentication_token}") + end + end + def build_commit_note(commit) notes.new(commit_id: commit.id, noteable_type: 'Commit') end @@ -1151,7 +1164,10 @@ class Project < ActiveRecord::Base def schedule_delete!(user_id, params) # Queue this task for after the commit, so once we mark pending_delete it will run - run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) } + run_after_commit do + job_id = ProjectDestroyWorker.perform_async(id, user_id, params) + Rails.logger.info("User #{user_id} scheduled destruction of project #{path_with_namespace} with job ID #{job_id}") + end update_attribute(:pending_delete, true) end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 23e5b16221b..d7c986c1a91 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -46,7 +46,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options) + gate[room].send('GitLab', message, message_options(data)) end def test(data) @@ -67,8 +67,8 @@ class HipchatService < Service @gate ||= HipChat::Client.new(token, options) end - def message_options - { notify: notify.present? && notify == '1', color: color || 'yellow' } + def message_options(data = nil) + { notify: notify.present? && notify == '1', color: message_color(data) } end def create_message(data) @@ -240,6 +240,21 @@ class HipchatService < Service "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" end + def message_color(data) + build_status_color(data) || color || 'yellow' + end + + def build_status_color(data) + return unless data && data[:object_kind] == 'build' + + case data[:commit][:status] + when 'success' + 'green' + else + 'red' + end + end + def project_name project.name_with_namespace.gsub(/\s/, '') end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 9d312a53790..fdfaf052730 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -132,39 +132,63 @@ class ProjectTeam Gitlab::Access.options_with_owner.key(max_member_access(user_id)) end - # This method assumes project and group members are eager loaded for optimal - # performance. + # Determine the maximum access level for a group of users in bulk. + # + # Returns a Hash mapping user ID -> maximum access level. + def max_member_access_for_user_ids(user_ids) + user_ids = user_ids.uniq + key = "max_member_access:#{project.id}" + RequestStore.store[key] ||= {} + access = RequestStore.store[key] + + # Lookup only the IDs we need + user_ids = user_ids - access.keys + + if user_ids.present? + user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS } + + member_access = project.members.access_for_user_ids(user_ids) + merge_max!(access, member_access) + + if group + group_access = group.members.access_for_user_ids(user_ids) + merge_max!(access, group_access) + end + + # Each group produces a list of maximum access level per user. We take the + # max of the values produced by each group. + if project.invited_groups.any? && project.allowed_to_share_with_group? + project.project_group_links.each do |group_link| + invited_access = max_invited_level_for_users(group_link, user_ids) + merge_max!(access, invited_access) + end + end + end + + access + end + def max_member_access(user_id) - access = [] - - access += project.members.where(user_id: user_id).has_access.pluck(:access_level) - - if group - access += group.members.where(user_id: user_id).has_access.pluck(:access_level) - end - - if project.invited_groups.any? && project.allowed_to_share_with_group? - access << max_invited_level(user_id) - end - - access.compact.max + max_member_access_for_user_ids([user_id])[user_id] end private - def max_invited_level(user_id) - project.project_group_links.map do |group_link| - invited_group = group_link.group - access = invited_group.group_members.find_by(user_id: user_id).try(:access_field) + # For a given group, return the maximum access level for the user. This is the min of + # the invited access level of the group and the access level of the user within the group. + # For example, if the group has been given DEVELOPER access but the member has MASTER access, + # the user should receive only DEVELOPER access. + def max_invited_level_for_users(group_link, user_ids) + invited_group = group_link.group + capped_access_level = group_link.group_access + access = invited_group.group_members.access_for_user_ids(user_ids) - # If group member has higher access level we should restrict it - # to max allowed access level - if access && access > group_link.group_access - access = group_link.group_access - end + # If the user is not in the list, assume he/she does not have access + missing_users = user_ids - access.keys + missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS } - access - end.compact.max + # Cap the maximum access by the invited level access + access.each { |key, value| access[key] = [value, capped_access_level].min } end def fetch_members(level = nil) @@ -215,4 +239,8 @@ class ProjectTeam def group project.group end + + def merge_max!(first_hash, second_hash) + first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index e9d5f4c91f8..af65e5b20ec 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -211,6 +211,9 @@ class Repository rugged.references.create(keep_around_ref_name(sha), sha, force: true) rescue Rugged::ReferenceError => ex Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" + rescue Rugged::OSError => ex + raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" end end @@ -612,11 +615,11 @@ class Repository case value when 'name' branches.sort_by(&:name) - when 'recently_updated' + when 'updated_desc' branches.sort do |a, b| commit(b.target).committed_date <=> commit(a.target).committed_date end - when 'last_updated' + when 'updated_asc' branches.sort do |a, b| commit(a.target).committed_date <=> commit(b.target).committed_date end @@ -985,6 +988,10 @@ class Repository if was_empty || !target_branch # Create branch rugged.references.create(ref, newrev) + + # If repo was empty expire cache + after_create if was_empty + after_create_branch else # Update head current_head = find_branch(branch).target diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index e63e1af8766..5e2de2ccf64 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -2,10 +2,14 @@ module Issues class CreateService < Issues::BaseService def execute filter_params - label_params = params[:label_ids] - issue = project.issues.new(params.except(:label_ids)) + label_params = params.delete(:label_ids) + request = params.delete(:request) + api = params.delete(:api) + issue = project.issues.new(params) issue.author = params[:author] || current_user + issue.spam = spam_check_service.execute(request, api) + if issue.save issue.update_attributes(label_ids: label_params) notification_service.new_issue(issue, current_user) @@ -17,5 +21,11 @@ module Issues issue end + + private + + def spam_check_service + SpamCheckService.new(project, current_user, params) + end end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index f06311511cc..921ca6748d3 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,7 +3,7 @@ module Projects def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] - + if new_visibility && new_visibility.to_i != project.visibility_level unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb new file mode 100644 index 00000000000..7c3e692bde9 --- /dev/null +++ b/app/services/spam_check_service.rb @@ -0,0 +1,38 @@ +class SpamCheckService < BaseService + include Gitlab::AkismetHelper + + attr_accessor :request, :api + + def execute(request, api) + @request, @api = request, api + return false unless request || check_for_spam?(project) + return false unless is_spam?(request.env, current_user, text) + + create_spam_log + + true + end + + private + + def text + [params[:title], params[:description]].reject(&:blank?).join("\n") + end + + def spam_log_attrs + { + user_id: current_user.id, + project_id: project.id, + title: params[:title], + description: params[:description], + source_ip: client_ip(request.env), + user_agent: user_agent(request.env), + noteable_type: 'Issue', + via_api: api + } + end + + def create_spam_log + CreateSpamLogService.new(project, current_user, spam_log_attrs).execute + end +end diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 9d722bd7382..89d7a40d6b0 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -16,3 +16,7 @@ = link_to admin_health_check_path, title: 'Health Check' do %span Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml new file mode 100644 index 00000000000..ae918086a57 --- /dev/null +++ b/app/views/admin/requests_profiles/index.html.haml @@ -0,0 +1,26 @@ +- @no_container = true +- page_title 'Requests Profiles' += render 'admin/background_jobs/head' + +%div{ class: container_class } + %h3.page-title + = page_title + + .bs-callout.clearfix + Pass the header + %code X-Profile-Token: #{@profile_token} + to profile the request + + - if @profiles.present? + .prepend-top-default + - @profiles.each do |path, profiles| + .panel.panel-default.panel-small + .panel-heading + %code= path + %ul.content-list + - profiles.each do |profile| + %li + = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true} + - else + %p + No profiles found diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 6e993e58f0d..15dd98077c8 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -74,6 +74,4 @@ = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" again. - -:javascript - new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index d3d3c595c17..c8a6fa1aa9e 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -56,5 +56,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } } diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 7486b1423e2..deaaf9af875 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -55,5 +55,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index aedb8468eca..fcfc6fd37f4 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -51,5 +51,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } } diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 267eee4f262..ed3afb0ce33 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -51,5 +51,4 @@ Import = icon("spinner spin", class: "loading-icon") -:javascript - new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } } diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 5ada6b174eb..e79f122940a 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -77,5 +77,4 @@ = link_to "import flow", new_import_google_code_path again. -:javascript - new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}"); +.js-importer-status{ data: { jobs_import_path: "#{jobs_import_google_code_path}", import_path: "#{import_google_code_path}" } } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 5ee8772882e..ac04f57e217 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -9,7 +9,7 @@ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: %w(system_info background_jobs logs health_check)) do + = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = link_to admin_system_info_path, title: 'Monitoring' do %span Monitoring diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 6f806e3ce53..e889f29c816 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -7,28 +7,28 @@ .nav-text Protected branches can be managed in project settings - - if can? current_user, :push_code, @project - .nav-controls - = form_tag(filter_branches_path, method: :get) do - = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if params[:sort].present? - = params[:sort].humanize - - else - Name - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to filter_branches_path(sort: nil) do - = sort_title_name - = link_to filter_branches_path(sort: 'recently_updated') do - = sort_title_recently_updated - = link_to filter_branches_path(sort: 'last_updated') do - = sort_title_oldest_updated + .nav-controls + = form_tag(filter_branches_path, method: :get) do + = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } + + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + = projects_sort_options_hash[@sort] + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to filter_branches_path(sort: sort_value_name) do + = sort_title_name + = link_to filter_branches_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to filter_branches_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + + - if can? current_user, :push_code, @project = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do New branch + - if @branches.any? %ul.content-list.all-branches - @branches.each do |branch| diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index b89183c40dc..a8bc53c2849 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -88,8 +88,9 @@ %p %span.build-light-text Variables: - %code - - @build.trigger_request.variables.each do |key, value| + + - @build.trigger_request.variables.each do |key, value| + %code #{key}=#{value} .block diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index a9fb3c58431..a3114771a42 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -22,6 +22,8 @@ - if defined?(ref) && ref - if build.ref + .icon-container + = build.tag? ? icon('tag') : icon('code-fork') = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 8ae433b4823..4bf3ccace20 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -7,7 +7,7 @@ .content-block.oneline-block.files-changed .inline-parallel-buttons - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? } - = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: 'html')), class: 'btn btn-default' + = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 921155e970b..b282aa52b25 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -4,6 +4,7 @@ %h4.prepend-top-0 Project settings .col-lg-9 + .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset.append-bottom-0 .form-group @@ -190,6 +191,7 @@ %h4.prepend-top-0.warning-title Rename repository .col-lg-9 + = render 'projects/errors' = form_for([@project.namespace.becomes(Namespace), @project]) do |f| .form-group.project_name_holder = f.label :name, class: 'label-light' do diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index c58223fd39e..195f18afc76 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -19,4 +19,9 @@ ] } var ctx = $("#build_timesChart").get(0).getContext("2d"); - new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); + var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } + new Chart(ctx).Bar(data, options); diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 8fca07114fa..1fbf6ca2c1c 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -48,4 +48,9 @@ ] } var ctx = $("##{scope}Chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); + var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } + new Chart(ctx).Line(data, options); diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 65db8af494d..7e34a89f9ae 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -59,6 +59,10 @@ var container = $(selector).parent(); var generateChart = function() { selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8 + } return new Chart(ctx).Bar(data, options); }; // enabling auto-resizing diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml new file mode 100644 index 00000000000..72669372497 --- /dev/null +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -0,0 +1,27 @@ +.issues-footer.text-center + %button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } } + Email a new issue to this project + +#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" } + .modal-dialog{ role: "document" } + .modal-content + .modal-header + %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } } + %span{ aria: { hidden: "true" } }= icon("times") + %h4.modal-title + Create new issue by email + .modal-body + %p + Write an email to the below email address. (This is a private email address, so keep it secret.) + .email-modal-input-group.input-group + = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true + .input-group-btn + = clipboard_button(clipboard_target: '#issue_email') + %p + Send an email to this address to create an issue. + %p + Use the subject line as the title of your issue. + %p + Use the message as the body of your issue (feel free to include some nice + = succeed ")." do + = link_to "Markdown", help_page_path('markdown', 'markdown') diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7612fe3719a..d0edd2f22ec 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,5 +1,6 @@ - @no_container = true - page_title "Issues" +- new_issue_email = @project.new_issue_address(current_user) = render "projects/issues/head" = content_for :meta_tags do @@ -23,7 +24,9 @@ = render 'shared/issuable/filter', type: :issues .issues-holder - = render "issues" + = render 'issues' + - if new_issue_email + = render 'issue_by_email', email: new_issue_email - else .blank-state.blank-state-welcome %h2.blank-state-title.blank-state-welcome-title @@ -40,3 +43,5 @@ - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue + - if new_issue_email + = render 'issue_by_email', email: new_issue_email diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 7d9bd08385a..dcf1f767bf7 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -6,4 +6,4 @@ $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.project-edit-content .btn-save').enable(); + $('.edit-project .btn-save').enable(); diff --git a/app/views/shared/icons/_icon_status_cancel.svg b/app/views/shared/icons/_icon_status_cancel.svg index 6a0bc1490c4..fd1ebbcbabd 100644 --- a/app/views/shared/icons/_icon_status_cancel.svg +++ b/app/views/shared/icons/_icon_status_cancel.svg @@ -1,12 +1,6 @@ - - - - - - - - - - + + + + diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg index c41ca18cae7..e56e0887416 100644 --- a/app/views/shared/icons/_icon_status_failed.svg +++ b/app/views/shared/icons/_icon_status_failed.svg @@ -1,12 +1,6 @@ - - - - - - - - - - + + + + diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg index 035cd8b4ccc..117f0367161 100644 --- a/app/views/shared/icons/_icon_status_pending.svg +++ b/app/views/shared/icons/_icon_status_pending.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - + + + + diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg index a48b3a25099..920d7952eb5 100644 --- a/app/views/shared/icons/_icon_status_running.svg +++ b/app/views/shared/icons/_icon_status_running.svg @@ -1,12 +1,6 @@ - - - - - - - - - - + + + + diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 260eab013a3..67b378b3571 100644 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1,15 +1,6 @@ - - - - - - - - - - - - - + + + + diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg index d47e7a1c93f..d0ad4bd65b1 100644 --- a/app/views/shared/icons/_icon_status_warning.svg +++ b/app/views/shared/icons/_icon_status_warning.svg @@ -1,15 +1,6 @@ - - - - - - - - - - - - - + + + + diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index f2649e38eb3..842eebdea9e 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -21,31 +21,35 @@ class EmailReceiverWorker return unless raw.present? can_retry = false - reason = nil + reason = + case e + when Gitlab::Email::UnknownIncomingEmail + "We couldn't figure out what the email is for. Please create your issue or comment through the web interface." + when Gitlab::Email::SentNotificationNotFoundError + "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." + when Gitlab::Email::ProjectNotFound + "We couldn't find the project. Please check if there's any typo." + when Gitlab::Email::EmptyEmailError + can_retry = true + "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." + when Gitlab::Email::AutoGeneratedEmailError + "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." + when Gitlab::Email::UserNotFoundError + "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." + when Gitlab::Email::UserBlockedError + "Your account has been blocked. If you believe this is in error, contact a staff member." + when Gitlab::Email::UserNotAuthorizedError + "You are not allowed to perform this action. If you believe this is in error, contact a staff member." + when Gitlab::Email::NoteableNotFoundError + "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." + when Gitlab::Email::InvalidNoteError, + Gitlab::Email::InvalidIssueError + can_retry = true + e.message + end - case e - when Gitlab::Email::Receiver::SentNotificationNotFoundError - reason = "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." - when Gitlab::Email::Receiver::EmptyEmailError - can_retry = true - reason = "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." - when Gitlab::Email::Receiver::AutoGeneratedEmailError - reason = "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." - when Gitlab::Email::Receiver::UserNotFoundError - reason = "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." - when Gitlab::Email::Receiver::UserBlockedError - reason = "Your account has been blocked. If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::UserNotAuthorizedError - reason = "You are not allowed to respond to the thread you are replying to. If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::NoteableNotFoundError - reason = "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." - when Gitlab::Email::Receiver::InvalidNoteError - can_retry = true - reason = e.message - else - return + if reason + EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end - - EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f7604e48f83..d69d6037053 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -4,7 +4,7 @@ class RepositoryForkWorker sidekiq_options queue: :gitlab_shell - def perform(project_id, source_path, target_path) + def perform(project_id, forked_from_repository_storage_path, source_path, target_path) project = Project.find_by_id(project_id) unless project.present? @@ -12,7 +12,8 @@ class RepositoryForkWorker return end - result = gitlab_shell.fork_repository(project.repository_storage_path, source_path, target_path) + result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, + project.repository_storage_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") project.mark_import_as_failed('The project could not be forked.') diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb new file mode 100644 index 00000000000..9dd228a2483 --- /dev/null +++ b/app/workers/requests_profiles_worker.rb @@ -0,0 +1,9 @@ +class RequestsProfilesWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + Gitlab::RequestProfiler.remove_all_profiles + end +end diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 293f2b71d65..74325872b09 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -68,6 +68,25 @@ :why: https://opensource.org/licenses/BSD-2-Clause :versions: [] :when: 2016-05-02 05:55:09.796363000 Z +- - :whitelist + - LGPLv2+ + - :who: Stan Hu + :why: Equivalent to LGPLv2 + :versions: [] + :when: 2016-06-07 17:14:10.907682000 Z +- - :whitelist + - Artistic 2.0 + - :who: Josh Frye + :why: Disk/mount information display on Admin pages + :versions: [] + :when: 2016-06-29 16:32:45.432113000 Z +- - :whitelist + - Simplified BSD + - :who: Douwe Maan + :why: https://opensource.org/licenses/BSD-2-Clause + :versions: [] + :when: 2016-07-26 21:24:07.248480000 Z + # LICENSE BLACKLIST - - :blacklist @@ -175,15 +194,3 @@ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z -- - :whitelist - - LGPLv2+ - - :who: Stan Hu - :why: Equivalent to LGPLv2 - :versions: [] - :when: 2016-06-07 17:14:10.907682000 Z -- - :whitelist - - Artistic 2.0 - - :who: Josh Frye - :why: Disk/mount information display on Admin pages - :versions: [] - :when: 2016-06-29 16:32:45.432113000 Z diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 86f55210487..49130f37b31 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -290,6 +290,9 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker' +Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' # # GitLab Shell diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index 37746968675..d92f64e1647 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -26,4 +26,4 @@ def validate_storages end end -validate_storages unless Rails.env.test? +validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true' diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb new file mode 100644 index 00000000000..fb5a7b8372e --- /dev/null +++ b/config/initializers/request_profiler.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::RequestProfiler::Middleware) +end diff --git a/config/routes.rb b/config/routes.rb index 40dce1745db..30f1134c573 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,10 +42,9 @@ Rails.application.routes.draw do resource :lint, only: [:show, :create] - resources :projects do + resources :projects, only: [:index, :show] do member do get :status, to: 'projects#badge' - get :integration end end @@ -144,13 +143,13 @@ Rails.application.routes.draw do get :jobs end - resource :gitlab, only: [:create, :new], controller: :gitlab do + resource :gitlab, only: [:create], controller: :gitlab do get :status get :callback get :jobs end - resource :bitbucket, only: [:create, :new], controller: :bitbucket do + resource :bitbucket, only: [:create], controller: :bitbucket do get :status get :callback get :jobs @@ -243,7 +242,6 @@ Rails.application.routes.draw do get :projects get :keys get :groups - put :team_update put :block put :unblock put :unlock @@ -281,6 +279,7 @@ Rails.application.routes.draw do resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] + resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do root to: 'projects#index', as: :projects @@ -300,7 +299,7 @@ Rails.application.routes.draw do end end - resource :appearances, path: 'appearance' do + resource :appearances, only: [:show, :create, :update], path: 'appearance' do member do get :preview delete :logo @@ -309,7 +308,7 @@ Rails.application.routes.draw do end resource :application_settings, only: [:show, :update] do - resources :services + resources :services, only: [:index, :edit, :update] put :reset_runners_token put :reset_health_check_token put :clear_repository_check_states @@ -346,7 +345,7 @@ Rails.application.routes.draw do end scope module: :profiles do - resource :account, only: [:show, :update] do + resource :account, only: [:show] do member do delete :unlink end @@ -358,7 +357,7 @@ Rails.application.routes.draw do end end resource :preferences, only: [:show, :update] - resources :keys + resources :keys, only: [:index, :show, :new, :create, :destroy] resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] @@ -660,7 +659,7 @@ Rails.application.routes.draw do post '/wikis/*id/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview' end - resource :repository, only: [:show, :create] do + resource :repository, only: [:create] do member do get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } end @@ -789,7 +788,7 @@ Rails.application.routes.draw do end end - resources :labels, constraints: { id: /\d+/ } do + resources :labels, except: [:show], constraints: { id: /\d+/ } do collection do post :generate post :set_priorities @@ -814,7 +813,7 @@ Rails.application.routes.draw do end end - resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do + resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do collection do delete :leave diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index b4639999967..e3316ecdb6c 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -20,7 +20,6 @@ Sidekiq::Testing.inline! do 'https://github.com/airbnb/javascript.git', 'https://github.com/tessalt/echo-chamber-js.git', 'https://github.com/atom/atom.git', - 'https://github.com/ipselon/react-ui-builder.git', 'https://github.com/mattermost/platform.git', 'https://github.com/purifycss/purifycss.git', 'https://github.com/facebook/nuclide.git', diff --git a/doc/api/commits.md b/doc/api/commits.md index 57c2e1d9b87..2960c2ae428 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -81,6 +81,11 @@ Example response: "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], + "stats": { + "additions": 15, + "deletions": 10, + "total": 25 + }, "status": "running" } ``` diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 5cc09bd536d..c222d21612f 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -1,9 +1,14 @@ # Akismet +> *Note:* Before 8.11 only issues submitted via the API and for non-project +members were submitted to Akismet. + GitLab leverages [Akismet](http://akismet.com) to protect against spam. Currently -GitLab uses Akismet to prevent users who are not members of a project from -creating spam via the GitLab API. Detected spam will be rejected, and -an entry in the "Spam Log" section in the Admin page will be created. +GitLab uses Akismet to prevent the creation of spam issues on public projects. Issues +created via the WebUI or the API can be submitted to Akismet for review. + +Detected spam will be rejected, and an entry in the "Spam Log" section in the +Admin page will be created. Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that adding a user to a project will disable the Akismet check and prevent this diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md new file mode 100644 index 00000000000..fc6262dd108 --- /dev/null +++ b/doc/update/8.10-to-8.11.md @@ -0,0 +1,191 @@ +# From 8.10 to 8.11 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-11-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-11-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v3.2.1 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.8 +sudo -u git -H make +``` + +### 6. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +# Login to MySQL +mysql -u root -p + +# Grant the GitLab user the REFERENCES permission on the database +GRANT REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost'; + +# Quit the database session +mysql> \q +``` + +### 7. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 8. 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-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-10-stable:lib/support/nginx/gitlab-ssl origin/8-11-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-10-stable:lib/support/nginx/gitlab origin/8-11-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-11-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-11-stable/config/initializers/smtp_settings.rb.sample#L13? + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 9. Start application + + sudo service gitlab start + sudo service nginx restart + +### 10. 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.10) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.9 to 8.10](8.9-to-8.10.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/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md index 71cbe5c8ac6..a057a423e61 100644 --- a/doc/update/8.9-to-8.10.md +++ b/doc/update/8.9-to-8.10.md @@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-10-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.2.0 +sudo -u git -H git checkout v3.2.1 ``` ### 5. Update gitlab-workhorse diff --git a/features/support/env.rb b/features/support/env.rb index f0a3dd8d2d0..569fd444e86 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,6 +1,5 @@ -if ENV['SIMPLECOV'] - require 'simplecov' -end +require './spec/simplecov_env' +SimpleCovEnv.start! ENV['RAILS_ENV'] = 'test' require './config/environment' diff --git a/lib/api/entities.rb b/lib/api/entities.rb index fbf0d74663f..e76e7304674 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -149,8 +149,13 @@ module API expose :safe_message, as: :message end + class RepoCommitStats < Grape::Entity + expose :additions, :deletions, :total + end + class RepoCommitDetail < RepoCommit expose :parent_ids, :committed_date, :authored_date + expose :stats, using: Entities::RepoCommitStats expose :status end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c588103e517..c4d3134da6c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -21,17 +21,6 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end - - def create_spam_log(project, current_user, attrs) - params = attrs.merge({ - source_ip: client_ip(env), - user_agent: user_agent(env), - noteable_type: 'Issue', - via_api: true - }) - - ::CreateSpamLogService.new(project, current_user, params).execute - end end resource :issues do @@ -168,15 +157,13 @@ module API end project = user_project - text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n") - if check_for_spam?(project, current_user) && is_spam?(env, current_user, text) - create_spam_log(project, current_user, attrs) + issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute + + if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end - issue = ::Issues::CreateService.new(project, current_user, attrs).execute - if issue.valid? # Find or create labels and attach to issue. Labels are valid because # we already checked its name, so there can't be an error here diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index de41ea415a6..a533bac2692 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -7,6 +7,7 @@ module Gitlab module Access class AccessDeniedError < StandardError; end + NO_ACCESS = 0 GUEST = 10 REPORTER = 20 DEVELOPER = 30 diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb index 04676fdb748..207736b59db 100644 --- a/lib/gitlab/akismet_helper.rb +++ b/lib/gitlab/akismet_helper.rb @@ -17,8 +17,8 @@ module Gitlab env['HTTP_USER_AGENT'] end - def check_for_spam?(project, user) - akismet_enabled? && !project.team.member?(user) + def check_for_spam?(project) + akismet_enabled? && project.public? end def is_spam?(environment, user, text) diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 34e0143a82e..839a4fa30d5 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -60,16 +60,18 @@ module Gitlab end # Fork repository to new namespace - # storage - project's storage path + # forked_from_storage - forked-from project's storage path # path - project path with namespace + # forked_to_storage - forked-to project's storage path # fork_namespace - namespace for forked project # # Ex. - # fork_repository("/path/to/storage", "gitlab/gitlab-ci", "randx") + # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "randx") # - def fork_repository(storage, path, fork_namespace) + def fork_repository(forked_from_storage, path, forked_to_storage, fork_namespace) Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project', - storage, "#{path}.git", fork_namespace]) + forked_from_storage, "#{path}.git", forked_to_storage, + fork_namespace]) end # Remove repository from file system diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb new file mode 100644 index 00000000000..bd3267e2a80 --- /dev/null +++ b/lib/gitlab/email/handler.rb @@ -0,0 +1,17 @@ +require 'gitlab/email/handler/create_note_handler' +require 'gitlab/email/handler/create_issue_handler' + +module Gitlab + module Email + module Handler + HANDLERS = [CreateNoteHandler, CreateIssueHandler] + + def self.for(mail, mail_key) + HANDLERS.find do |klass| + handler = klass.new(mail, mail_key) + break handler if handler.can_handle? + end + end + end + end +end diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb new file mode 100644 index 00000000000..b7ed11cb638 --- /dev/null +++ b/lib/gitlab/email/handler/base_handler.rb @@ -0,0 +1,60 @@ +module Gitlab + module Email + module Handler + class BaseHandler + attr_reader :mail, :mail_key + + def initialize(mail, mail_key) + @mail = mail + @mail_key = mail_key + end + + def message + @message ||= process_message + end + + def author + raise NotImplementedError + end + + def project + raise NotImplementedError + end + + private + + def validate_permission!(permission) + raise UserNotFoundError unless author + raise UserBlockedError if author.blocked? + raise ProjectNotFound unless author.can?(:read_project, project) + raise UserNotAuthorizedError unless author.can?(permission, project) + end + + def process_message + message = ReplyParser.new(mail).execute.strip + add_attachments(message) + end + + def add_attachments(reply) + attachments = Email::AttachmentUploader.new(mail).execute(project) + + reply + attachments.map do |link| + "\n\n#{link[:markdown]}" + end.join + end + + def verify_record!(record:, invalid_exception:, record_name:) + return if record.persisted? + + error_title = "The #{record_name} could not be created for the following reasons:" + + msg = error_title + record.errors.full_messages.map do |error| + "\n\n- #{error}" + end.join + + raise invalid_exception, msg + end + end + end + end +end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb new file mode 100644 index 00000000000..4e6566af8ab --- /dev/null +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -0,0 +1,52 @@ + +require 'gitlab/email/handler/base_handler' + +module Gitlab + module Email + module Handler + class CreateIssueHandler < BaseHandler + attr_reader :project_path, :authentication_token + + def initialize(mail, mail_key) + super(mail, mail_key) + @project_path, @authentication_token = + mail_key && mail_key.split('+', 2) + end + + def can_handle? + !authentication_token.nil? + end + + def execute + raise ProjectNotFound unless project + + validate_permission!(:create_issue) + + verify_record!( + record: create_issue, + invalid_exception: InvalidIssueError, + record_name: 'issue') + end + + def author + @author ||= User.find_by(authentication_token: authentication_token) + end + + def project + @project ||= Project.find_with_namespace(project_path) + end + + private + + def create_issue + Issues::CreateService.new( + project, + author, + title: mail.subject, + description: message + ).execute + end + end + end + end +end diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb new file mode 100644 index 00000000000..06dae31cc27 --- /dev/null +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -0,0 +1,55 @@ + +require 'gitlab/email/handler/base_handler' + +module Gitlab + module Email + module Handler + class CreateNoteHandler < BaseHandler + def can_handle? + mail_key =~ /\A\w+\z/ + end + + def execute + raise SentNotificationNotFoundError unless sent_notification + raise AutoGeneratedEmailError if mail.header.to_s =~ /auto-(generated|replied)/ + + validate_permission!(:create_note) + + raise NoteableNotFoundError unless sent_notification.noteable + raise EmptyEmailError if message.blank? + + verify_record!( + record: create_note, + invalid_exception: InvalidNoteError, + record_name: 'comment') + end + + def author + sent_notification.recipient + end + + def project + sent_notification.project + end + + def sent_notification + @sent_notification ||= SentNotification.for(mail_key) + end + + private + + def create_note + Notes::CreateService.new( + project, + author, + note: message, + noteable_type: sent_notification.noteable_type, + noteable_id: sent_notification.noteable_id, + commit_id: sent_notification.commit_id, + line_code: sent_notification.line_code + ).execute + end + end + end + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 1c671a7487b..9213cfb51e8 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -1,18 +1,24 @@ + +require 'gitlab/email/handler' + # Inspired in great part by Discourse's Email::Receiver module Gitlab module Email - class Receiver - class ProcessingError < StandardError; end - class EmailUnparsableError < ProcessingError; end - class SentNotificationNotFoundError < ProcessingError; end - class EmptyEmailError < ProcessingError; end - class AutoGeneratedEmailError < ProcessingError; end - class UserNotFoundError < ProcessingError; end - class UserBlockedError < ProcessingError; end - class UserNotAuthorizedError < ProcessingError; end - class NoteableNotFoundError < ProcessingError; end - class InvalidNoteError < ProcessingError; end + class ProcessingError < StandardError; end + class EmailUnparsableError < ProcessingError; end + class SentNotificationNotFoundError < ProcessingError; end + class ProjectNotFound < ProcessingError; end + class EmptyEmailError < ProcessingError; end + class AutoGeneratedEmailError < ProcessingError; end + class UserNotFoundError < ProcessingError; end + class UserBlockedError < ProcessingError; end + class UserNotAuthorizedError < ProcessingError; end + class NoteableNotFoundError < ProcessingError; end + class InvalidNoteError < ProcessingError; end + class InvalidIssueError < ProcessingError; end + class UnknownIncomingEmail < ProcessingError; end + class Receiver def initialize(raw) @raw = raw end @@ -20,91 +26,38 @@ module Gitlab def execute raise EmptyEmailError if @raw.blank? - raise SentNotificationNotFoundError unless sent_notification + mail = build_mail + mail_key = extract_mail_key(mail) + handler = Handler.for(mail, mail_key) - raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/ + raise UnknownIncomingEmail unless handler - author = sent_notification.recipient - - raise UserNotFoundError unless author - - raise UserBlockedError if author.blocked? - - project = sent_notification.project - - raise UserNotAuthorizedError unless project && author.can?(:create_note, project) - - raise NoteableNotFoundError unless sent_notification.noteable - - reply = ReplyParser.new(message).execute.strip - - raise EmptyEmailError if reply.blank? - - reply = add_attachments(reply) - - note = create_note(reply) - - unless note.persisted? - msg = "The comment could not be created for the following reasons:" - note.errors.full_messages.each do |error| - msg << "\n\n- #{error}" - end - - raise InvalidNoteError, msg - end + handler.execute end - private - - def message - @message ||= Mail::Message.new(@raw) - rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e + def build_mail + Mail::Message.new(@raw) + rescue Encoding::UndefinedConversionError, + Encoding::InvalidByteSequenceError => e raise EmailUnparsableError, e end - def reply_key - key_from_to_header || key_from_additional_headers + def extract_mail_key(mail) + key_from_to_header(mail) || key_from_additional_headers(mail) end - def key_from_to_header - key = nil - message.to.each do |address| + def key_from_to_header(mail) + mail.to.find do |address| key = Gitlab::IncomingEmail.key_from_address(address) - break if key + break key if key end - - key end - def key_from_additional_headers - reply_key = nil - - Array(message.references).each do |message_id| - reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id) - break if reply_key + def key_from_additional_headers(mail) + Array(mail.references).find do |mail_id| + key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) + break key if key end - - reply_key - end - - def sent_notification - return nil unless reply_key - - SentNotification.for(reply_key) - end - - def add_attachments(reply) - attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project) - - attachments.each do |link| - reply << "\n\n#{link[:markdown]}" - end - - reply - end - - def create_note(reply) - sent_notification.create_note(reply) end end end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 8ce9d32abe0..d7be50bd437 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,7 +1,7 @@ module Gitlab module IncomingEmail class << self - FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze + FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze def enabled? config.enabled && config.address @@ -21,8 +21,8 @@ module Gitlab match[1] end - def key_from_fallback_reply_message_id(message_id) - match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX) + def key_from_fallback_message_id(mail_id) + match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX) return unless match match[1] diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 49f702f91f6..86a5b9a201a 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -124,6 +124,11 @@ module Gitlab trans.action = action if trans end + # Returns the prefix to use for the name of a series. + def self.series_prefix + @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' + end + # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. if enabled? diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index dcec7543c13..4b7a791e497 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -9,14 +9,17 @@ module Gitlab # # Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login) module Instrumentation - SERIES = 'method_calls' - PROXY_IVAR = :@__gitlab_instrumentation_proxy def self.configure yield self end + # Returns the name of the series to use for storing method calls. + def self.series + @series ||= "#{Metrics.series_prefix}method_calls" + end + # Instruments a class method. # # mod - The module to instrument as a Module/Class. @@ -141,15 +144,15 @@ module Gitlab # generated method _only_ accepts regular arguments if the underlying # method also accepts them. if method.arity == 0 - args_signature = '&block' + args_signature = '' else - args_signature = '*args, &block' + args_signature = '*args' end proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) if trans = Gitlab::Metrics::Instrumentation.transaction - trans.measure_method(#{label.inspect}) { super } + trans.method_call_for(#{label.to_sym.inspect}).measure { super } else super end diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb index c048fe20ba7..d3465e5ec19 100644 --- a/lib/gitlab/metrics/method_call.rb +++ b/lib/gitlab/metrics/method_call.rb @@ -11,8 +11,8 @@ module Gitlab def initialize(name, series) @name = name @series = series - @real_time = 0.0 - @cpu_time = 0.0 + @real_time = 0 + @cpu_time = 0 @call_count = 0 end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index 82c18bb108b..287b7a83547 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -35,12 +35,12 @@ module Gitlab if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID) def self.cpu_time Process. - clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond).to_f + clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond) end else def self.cpu_time Process. - clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond).to_f + clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond) end end @@ -48,14 +48,14 @@ module Gitlab # # Returns the time as a Float. def self.real_time(precision = :millisecond) - Process.clock_gettime(Process::CLOCK_REALTIME, precision).to_f + Process.clock_gettime(Process::CLOCK_REALTIME, precision) end # Returns the current monotonic clock time in a given precision. # # Returns the time as a Float. def self.monotonic_time(precision = :millisecond) - Process.clock_gettime(Process::CLOCK_MONOTONIC, precision).to_f + Process.clock_gettime(Process::CLOCK_MONOTONIC, precision) end end end diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index bded245da43..968f3218950 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -52,23 +52,16 @@ module Gitlab end def add_metric(series, values, tags = {}) - @metrics << Metric.new("#{series_prefix}#{series}", values, tags) + @metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags) end - # Measures the time it takes to execute a method. - # - # Multiple calls to the same method add up to the total runtime of the - # method. - # - # name - The full name of the method to measure (e.g. `User#sign_in`). - def measure_method(name, &block) - unless @methods[name] - series = "#{series_prefix}#{Instrumentation::SERIES}" - - @methods[name] = MethodCall.new(name, series) + # Returns a MethodCall object for the given name. + def method_call_for(name) + unless method = @methods[name] + @methods[name] = method = MethodCall.new(name, Instrumentation.series) end - @methods[name].measure(&block) + method end def increment(name, value) @@ -115,14 +108,6 @@ module Gitlab Metrics.submit_metrics(submit_hashes) end - - def sidekiq? - Sidekiq.server? - end - - def series_prefix - sidekiq? ? 'sidekiq_' : 'rails_' - end end end end diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb new file mode 100644 index 00000000000..8130e55351e --- /dev/null +++ b/lib/gitlab/request_profiler.rb @@ -0,0 +1,19 @@ +require 'fileutils' + +module Gitlab + module RequestProfiler + PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles" + + def profile_token + Rails.cache.fetch('profile-token') do + Devise.friendly_token + end + end + module_function :profile_token + + def remove_all_profiles + FileUtils.rm_rf(PROFILES_DIR) + end + module_function :remove_all_profiles + end +end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb new file mode 100644 index 00000000000..8da8b754975 --- /dev/null +++ b/lib/gitlab/request_profiler/middleware.rb @@ -0,0 +1,47 @@ +require 'ruby-prof' + +module Gitlab + module RequestProfiler + class Middleware + def initialize(app) + @app = app + end + + def call(env) + if profile?(env) + call_with_profiling(env) + else + @app.call(env) + end + end + + def profile?(env) + header_token = env['HTTP_X_PROFILE_TOKEN'] + return unless header_token.present? + + profile_token = RequestProfiler.profile_token + return unless profile_token.present? + + header_token == profile_token + end + + def call_with_profiling(env) + ret = nil + result = RubyProf::Profile.profile do + ret = @app.call(env) + end + + printer = RubyProf::CallStackPrinter.new(result) + file_name = "#{env['PATH_INFO'].tr('/', '|')}_#{Time.current.to_i}.html" + file_path = "#{PROFILES_DIR}/#{file_name}" + + FileUtils.mkdir_p(PROFILES_DIR) + File.open(file_path, 'wb') do |file| + printer.print(file) + end + + ret + end + end + end +end diff --git a/lib/gitlab/request_profiler/profile.rb b/lib/gitlab/request_profiler/profile.rb new file mode 100644 index 00000000000..f89d56903ef --- /dev/null +++ b/lib/gitlab/request_profiler/profile.rb @@ -0,0 +1,43 @@ +module Gitlab + module RequestProfiler + class Profile + attr_reader :name, :time, :request_path + + alias_method :to_param, :name + + def self.all + Dir["#{PROFILES_DIR}/*.html"].map do |path| + new(File.basename(path)) + end + end + + def self.find(name) + name_dup = name.dup + name_dup << '.html' unless name.end_with?('.html') + + file_path = "#{PROFILES_DIR}/#{name_dup}" + return unless File.exist?(file_path) + + new(name_dup) + end + + def initialize(name) + @name = name + + set_attributes + end + + def content + File.read("#{PROFILES_DIR}/#{name}") + end + + private + + def set_attributes + _, path, timestamp = name.split(/(.*)_(\d+)\.html$/) + @request_path = path.tr('|', '/') + @time = Time.at(timestamp.to_i).utc + end + end + end +end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 21c0e5f1d41..d3dcbd2c29b 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -7,5 +7,5 @@ end unless Rails.env.production? desc "GitLab | Run all tests on CI with simplecov" - task test_ci: [:rubocop, :brakeman, 'teaspoon', :spinach, :spec] + task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec] end diff --git a/public/404.html b/public/404.html index 4862770cc2a..92b7f4da0b9 100644 --- a/public/404.html +++ b/public/404.html @@ -1,55 +1,65 @@ + The page you're looking for could not be found (404)

-
+ GitLab Logo
404

-

The page you're looking for could not be found.

-
-

Make sure the address is correct and that the page hasn't moved.

-

Please contact your GitLab administrator if you think this is a mistake.

+
+

The page you're looking for could not be found.

+
+

Make sure the address is correct and that the page hasn't moved.

+

Please contact your GitLab administrator if you think this is a mistake.

+
diff --git a/public/422.html b/public/422.html index 055b0bde165..f625f8a33b7 100644 --- a/public/422.html +++ b/public/422.html @@ -1,55 +1,65 @@ + The change you requested was rejected (422) + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + + img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } +

-
+ GitLab Logo
422

-

The change you requested was rejected.

-
-

Make sure you have access to the thing you tried to change.

-

Please contact your GitLab administrator if you think this is a mistake.

+
+

The change you requested was rejected.

+
+

Make sure you have access to the thing you tried to change.

+

Please contact your GitLab administrator if you think this is a mistake.

+
diff --git a/public/500.html b/public/500.html index 3d59d1392f5..d76c66ba92a 100644 --- a/public/500.html +++ b/public/500.html @@ -1,54 +1,65 @@ + Something went wrong (500) +

-
+ GitLab Logo
500

-

Whoops, something went wrong on our end.

-
-

Try refreshing the page, or going back and attempting the action again.

-

Please contact your GitLab administrator if this problem persists.

+
+

Whoops, something went wrong on our end.

+
+

Try refreshing the page, or going back and attempting the action again.

+

Please contact your GitLab administrator if this problem persists.

+
diff --git a/public/502.html b/public/502.html index 67dfd8a2743..1a3c7efc769 100644 --- a/public/502.html +++ b/public/502.html @@ -1,14 +1,13 @@ + GitLab is not responding (502) +

-
+ GitLab Logo
502

-

Whoops, GitLab is taking too much time to respond.

-
-

Try refreshing the page, or going back and attempting the action again.

-

Please contact your GitLab administrator if this problem persists.

+
+

Whoops, GitLab is taking too much time to respond.

+
+

Try refreshing the page, or going back and attempting the action again.

+

Please contact your GitLab administrator if this problem persists.

+
diff --git a/public/503.html b/public/503.html index 6ab1185658d..c1c4e3ffdb8 100644 --- a/public/503.html +++ b/public/503.html @@ -1,14 +1,13 @@ + GitLab is not responding (503) +

- GitLab Logo
+ GitLab Logo
503

-

Whoops, GitLab is currently unavailable.

-
-

Try refreshing the page, or going back and attempting the action again.

-

Please contact your GitLab administrator if this problem persists.

+
+

Whoops, GitLab is currently unavailable.

+
+

Try refreshing the page, or going back and attempting the action again.

+

Please contact your GitLab administrator if this problem persists.

+
diff --git a/public/deploy.html b/public/deploy.html index 48976dacf41..142472b6c35 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -1,54 +1,64 @@ - - Deploy in progress - - + hr { + max-width: 800px; + margin: 18px auto; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } - -

-
- Deploy in progress -

+ img { + max-width: 40vw; + } + + .container { + margin: auto 20px; + } + + + + +

+ GitLab Logo
+ Deploy in progress +

+

Please try again in a few minutes.

-
+

Please contact your GitLab administrator if this problem persists.

- +
+ diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov new file mode 100755 index 00000000000..65f93f8830b --- /dev/null +++ b/scripts/merge-simplecov @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby + +require_relative '../spec/simplecov_env' +SimpleCovEnv.configure_profile + +module SimpleCov + module ResultMerger + class << self + def resultset_files + Dir.glob(File.join(SimpleCov.coverage_path, '*', '.resultset.json')) + end + + def resultset_hashes + resultset_files.map do |path| + begin + JSON.parse(File.read(path)) + rescue + {} + end + end + end + + def resultset + resultset_hashes.reduce({}, :merge) + end + end + end +end + +SimpleCov::ResultMerger.merged_result.format! diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 60c654f622d..ed0b7f9e240 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -163,4 +163,17 @@ describe AutocompleteController do expect(body.collect { |u| u['id'] }).not_to include(99999) end end + + context 'skip_users parameter included' do + before { sign_in(user) } + + it 'skips the user IDs passed' do + get(:users, skip_users: [user, user2].map(&:id)) + + other_user_ids = [non_member, project.owner, project.creator].map(&:id) + response_user_ids = JSON.parse(response.body).map { |user| user['id'] } + + expect(response_user_ids).to contain_exactly(*other_user_ids) + end + end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7cf09fa4a4a..77f65057f71 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -243,6 +243,37 @@ describe Projects::IssuesController do end end + describe 'POST #create' do + context 'Akismet is enabled' do + before do + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) + end + + def post_spam_issue + sign_in(user) + spam_project = create(:empty_project, :public) + post :create, { + namespace_id: spam_project.namespace.to_param, + project_id: spam_project.to_param, + issue: { title: 'Spam Title', description: 'Spam lives here' } + } + end + + it 'rejects an issue recognized as spam' do + expect{ post_spam_issue }.not_to change(Issue, :count) + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + post_spam_issue + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('Spam Title') + end + end + end + describe "DELETE #destroy" do context "when the user is a developer" do before { sign_in(user) } diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb new file mode 100644 index 00000000000..a6995145cc1 --- /dev/null +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Projects::TagsController do + let(:project) { create(:project, :public) } + let!(:release) { create(:release, project: project) } + let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } + + describe 'GET index' do + before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param } + + it 'returns the tags for the page' do + expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) + end + + it 'returns releases matching those tags' do + expect(assigns(:releases)).to include(release) + expect(assigns(:releases)).not_to include(invalid_release) + end + end +end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index 6d47d05f8ad..b8d8fab0e0b 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -5,7 +5,8 @@ FactoryGirl.define do variables do { - TRIGGER_KEY: 'TRIGGER_VALUE' + TRIGGER_KEY_1: 'TRIGGER_VALUE_1', + TRIGGER_KEY_2: 'TRIGGER_VALUE_2' } end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d51c9abea19..93dcb2ec3fc 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -524,6 +524,35 @@ describe 'Issues', feature: true do end end + describe 'new issue by email' do + shared_examples 'show the email in the modal' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + + visit namespace_project_issues_path(project.namespace, project) + click_button('Email a new issue') + end + + it 'click the button to show modal for the new email' do + page.within '#issue-email-modal' do + email = project.new_issue_address(@user) + + expect(page).to have_selector("input[value='#{email}']") + end + end + end + + context 'with existing issues' do + let!(:issue) { create(:issue, project: project, author: @user) } + + it_behaves_like 'show the email in the modal' + end + + context 'without existing issues' do + it_behaves_like 'show the email in the modal' + end + end + describe 'due date' do context 'update due on issue#show', js: true do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } diff --git a/spec/features/projects/branches_spec.rb~HEAD b/spec/features/projects/branches_spec.rb~HEAD deleted file mode 100644 index 79abba21854..00000000000 --- a/spec/features/projects/branches_spec.rb~HEAD +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe 'Branches', feature: true do - let(:project) { create(:project) } - let(:repository) { project.repository } - - before do - login_as :user - project.team << [@user, :developer] - end - - describe 'Initial branches page' do - it 'shows all the branches' do - visit namespace_project_branches_path(project.namespace, project) - - repository.branches { |branch| expect(page).to have_content("#{branch.name}") } - expect(page).to have_content("Protected branches can be managed in project settings") - end - end - - describe 'Find branches' do - it 'shows filtered branches', js: true do - visit namespace_project_branches_path(project.namespace, project, project.id) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - end - end -end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb new file mode 100644 index 00000000000..3de25d7af7d --- /dev/null +++ b/spec/features/projects/project_settings_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'Edit Project Settings', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } + + before do + login_as(user) + project.team << [user, :master] + end + + describe 'Project settings', js: true do + it 'shows errors for invalid project name' do + visit edit_namespace_project_path(project.namespace, project) + + fill_in 'project_name_edit', with: 'foo&bar' + + click_button 'Save changes' + + expect(page).to have_field 'project_name_edit', with: 'foo&bar' + expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'." + expect(page).to have_button 'Save changes' + end + end + + describe 'Rename repository' do + it 'shows errors for invalid project path/name' do + visit edit_namespace_project_path(project.namespace, project) + + fill_in 'Project name', with: 'foo&bar' + fill_in 'Path', with: 'foo&bar' + + click_button 'Rename project' + + expect(page).to have_field 'Project name', with: 'foo&bar' + expect(page).to have_field 'Path', with: 'foo&bar' + expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'." + expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" + end + end +end diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index dd85203a038..6ea9a3a3ec5 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -16,7 +16,7 @@ describe BranchesFinder do end it 'sorts by recently_updated' do - branches_finder = described_class.new(repository, { sort: 'recently_updated' }) + branches_finder = described_class.new(repository, { sort: 'updated_desc' }) result = branches_finder.execute @@ -24,7 +24,7 @@ describe BranchesFinder do end it 'sorts by last_updated' do - branches_finder = described_class.new(repository, { sort: 'last_updated' }) + branches_finder = described_class.new(repository, { sort: 'updated_asc' }) result = branches_finder.execute @@ -53,7 +53,7 @@ describe BranchesFinder do context 'filter and sort' do it 'filters branches by name and sorts by recently_updated' do - params = { sort: 'recently_updated', search: 'feature' } + params = { sort: 'updated_desc', search: 'feature' } branches_finder = described_class.new(repository, params) result = branches_finder.execute @@ -63,7 +63,7 @@ describe BranchesFinder do end it 'filters branches by name and sorts by last_updated' do - params = { sort: 'last_updated', search: 'feature' } + params = { sort: 'updated_asc', search: 'feature' } branches_finder = described_class.new(repository, params) result = branches_finder.execute diff --git a/spec/fixtures/emails/valid_new_issue.eml b/spec/fixtures/emails/valid_new_issue.eml new file mode 100644 index 00000000000..3cf53a656a5 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue.eml @@ -0,0 +1,23 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +The reply by email functionality should be extended to allow creating a new issue by email. + +* Allow an admin to specify which project the issue should be created under by checking the sender domain. +* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under. diff --git a/spec/fixtures/emails/valid_new_issue_empty.eml b/spec/fixtures/emails/valid_new_issue_empty.eml new file mode 100644 index 00000000000..fc1d52a3f42 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue_empty.eml @@ -0,0 +1,18 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_authentication_token.eml new file mode 100644 index 00000000000..0994c2f7775 --- /dev/null +++ b/spec/fixtures/emails/wrong_authentication_token.eml @@ -0,0 +1,18 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: incoming+gitlabhq/gitlabhq+bad_token@appmail.adventuretime.ooo +Message-ID: +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_mail_key.eml similarity index 100% rename from spec/fixtures/emails/wrong_reply_key.eml rename to spec/fixtures/emails/wrong_mail_key.eml diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 08a93503258..af371248ae9 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -1,37 +1,30 @@ require "spec_helper" describe NotesHelper do + let(:owner) { create(:owner) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, namespace: group) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + + let(:owner_note) { create(:note, author: owner, project: project) } + let(:master_note) { create(:note, author: master, project: project) } + let(:reporter_note) { create(:note, author: reporter, project: project) } + let!(:notes) { [owner_note, master_note, reporter_note] } + + before do + group.add_owner(owner) + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + describe "#notes_max_access_for_users" do - let(:owner) { create(:owner) } - let(:group) { create(:group) } - let(:project) { create(:empty_project, namespace: group) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:owner_note) { create(:note, author: owner, project: project) } - let(:master_note) { create(:note, author: master, project: project) } - let(:reporter_note) { create(:note, author: reporter, project: project) } - let!(:notes) { [owner_note, master_note, reporter_note] } - - before do - group.add_owner(owner) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end - it 'return human access levels' do - original_method = project.team.method(:human_max_access) - expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args| - original_method.call(args[1]) - end - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') expect(helper.note_max_access_for_user(master_note)).to eq('Master') expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') - # Call it again to ensure value is cached - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') end it 'handles access in different projects' do @@ -43,4 +36,16 @@ describe NotesHelper do expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') end end + + describe '#preload_max_access_for_authors' do + it 'loads multiple users' do + expected_access = { + owner.id => Gitlab::Access::OWNER, + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER + } + + expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access) + end + end end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 88a71528867..b08396da4d2 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::AkismetHelper, type: :helper do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } before do @@ -11,13 +11,13 @@ describe Gitlab::AkismetHelper, type: :helper do end describe '#check_for_spam?' do - it 'returns true for non-member' do - expect(helper.check_for_spam?(project, user)).to eq(true) + it 'returns true for public project' do + expect(helper.check_for_spam?(project)).to eq(true) end - it 'returns false for member' do - project.team << [user, :guest] - expect(helper.check_for_spam?(project, user)).to eq(false) + it 'returns false for private project' do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + expect(helper.check_for_spam?(project)).to eq(false) end end diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb new file mode 100644 index 00000000000..19298e261e3 --- /dev/null +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -0,0 +1,41 @@ +require 'gitlab/email/receiver' + +shared_context :email_shared_context do + let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } + let(:receiver) { Gitlab::Email::Receiver.new(email_raw) } + let(:markdown) { "![image](uploads/image.png)" } + + def setup_attachment + allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( + [ + { + url: "uploads/image.png", + alt: "image", + markdown: markdown + } + ] + ) + end +end + +shared_examples :email_shared_examples do + context "when the user could not be found" do + before do + user.destroy + end + + it "raises a UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when the user is not authorized to the project" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + it "raises a ProjectNotFound" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb new file mode 100644 index 00000000000..e1153154778 --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_new_issue.eml') } + let(:namespace) { create(:namespace, path: 'gitlabhq') } + + let!(:project) { create(:project, :public, namespace: namespace) } + let!(:user) do + create( + :user, + email: 'jake@adventuretime.ooo', + authentication_token: 'auth_token' + ) + end + + context "when everything is fine" do + it "creates a new issue" do + setup_attachment + + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to include('reply by email') + expect(issue.description).to include(markdown) + end + + context "when the reply is blank" do + let(:email_raw) { fixture_file("emails/valid_new_issue_empty.eml") } + + it "creates a new issue" do + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to eq('') + end + end + end + + context "something is wrong" do + context "when the issue could not be saved" do + before do + allow_any_instance_of(Issue).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidIssueError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidIssueError) + end + end + + context "when we can't find the authentication_token" do + let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") } + + it "raises an UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when project is private" do + let(:project) { create(:project, :private, namespace: namespace) } + + it "raises a ProjectNotFound if the user is not a member" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb new file mode 100644 index 00000000000..a2119b0dadf --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml') } + let(:project) { create(:project, :public) } + let(:noteable) { create(:issue, project: project) } + let(:user) { create(:user) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context "when the recipient address doesn't include a mail key" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } + + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end + + context "when no sent notification for the mail key could be found" do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it "raises a SentNotificationNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end + + context "when the email was auto generated" do + let!(:mail_key) { '636ca428858779856c226bb145ef4fad' } + let!(:email_raw) { fixture_file("emails/auto_reply.eml") } + + it "raises an AutoGeneratedEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError) + end + end + + context "when the noteable could not be found" do + before do + noteable.destroy + end + + it "raises a NoteableNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context "when the note could not be saved" do + before do + allow_any_instance_of(Note).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidNoteError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + end + + context "when the reply is blank" do + let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } + + it "raises an EmptyEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) + end + end + + context "when everything is fine" do + before do + setup_attachment + end + + it "creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include("I could not disagree more.") + end + + it "adds all attachments" do + receiver.execute + + note = noteable.notes.last + + expect(note.note).to include(markdown) + end + + context 'when sub-addressing is not supported' do + before do + stub_incoming_email_setting(enabled: true, address: nil) + end + + shared_examples 'an email that contains a mail key' do |header| + it "fetches the mail key from the #{header} header and creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include('I could not disagree more.') + end + end + + context 'mail key is in the References header' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end + end + end +end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 84d2584a791..2a86b427806 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -1,34 +1,14 @@ -require "spec_helper" +require 'spec_helper' +require_relative 'email_shared_blocks' describe Gitlab::Email::Receiver, lib: true do - before do - stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") - stub_config_setting(host: 'localhost') - end + include_context :email_shared_context - let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } - let(:email_raw) { fixture_file('emails/valid_reply.eml') } + context "when we cannot find a capable handler" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") } - let(:project) { create(:project, :public) } - let(:noteable) { create(:issue, project: project) } - let(:user) { create(:user) } - let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) } - - let(:receiver) { described_class.new(email_raw) } - - context "when the recipient address doesn't include a reply key" do - let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") } - - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) - end - end - - context "when no sent notificiation for the reply key could be found" do - let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') } - - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end end @@ -36,128 +16,7 @@ describe Gitlab::Email::Receiver, lib: true do let(:email_raw) { "" } it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the email was auto generated" do - let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } - let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - - it "raises an AutoGeneratedEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) - end - end - - context "when the user could not be found" do - before do - user.destroy - end - - it "raises a UserNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError) - end - end - - context "when the user has been blocked" do - before do - user.block - end - - it "raises a UserBlockedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError) - end - end - - context "when the user is not authorized to create a note" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - it "raises a UserNotAuthorizedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError) - end - end - - context "when the noteable could not be found" do - before do - noteable.destroy - end - - it "raises a NoteableNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError) - end - end - - context "when the reply is blank" do - let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - - it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the note could not be saved" do - before do - allow_any_instance_of(Note).to receive(:persisted?).and_return(false) - end - - it "raises an InvalidNoteError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError) - end - end - - context "when everything is fine" do - let(:markdown) { "![image](uploads/image.png)" } - - before do - allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( - [ - { - url: "uploads/image.png", - alt: "image", - markdown: markdown - } - ] - ) - end - - it "creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include("I could not disagree more.") - end - - it "adds all attachments" do - receiver.execute - - note = noteable.notes.last - - expect(note.note).to include(markdown) - end - - context 'when sub-addressing is not supported' do - before do - stub_incoming_email_setting(enabled: true, address: nil) - end - - shared_examples 'an email that contains a reply key' do |header| - it "fetches the reply key from the #{header} header and creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include('I could not disagree more.') - end - end - - context 'reply key is in the References header' do - let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } - - it_behaves_like 'an email that contains a reply key', 'References' - end + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) end end end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index afb3e26f8fb..1dcf2c0668b 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -43,9 +43,9 @@ describe Gitlab::IncomingEmail, lib: true do end end - context 'self.key_from_fallback_reply_message_id' do + context 'self.key_from_fallback_message_id' do it 'returns reply key' do - expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key') + expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 8809b7e3f12..d88bcae41fb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Metrics::Instrumentation do allow(@dummy).to receive(:name).and_return('Dummy') end + describe '.series' do + it 'returns a String' do + expect(described_class.series).to be_an_instance_of(String) + end + end + describe '.configure' do it 'yields self' do described_class.configure do |c| @@ -78,8 +84,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy.foo') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.foo end @@ -157,8 +162,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy#bar') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.new.bar end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index cf0e282c2fb..9e2ea89a712 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -28,20 +28,20 @@ describe Gitlab::Metrics::System do end describe '.cpu_time' do - it 'returns a Float' do - expect(described_class.cpu_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.cpu_time).to be_an_instance_of(Fixnum) end end describe '.real_time' do - it 'returns a Float' do - expect(described_class.real_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.real_time).to be_an_instance_of(Fixnum) end end describe '.monotonic_time' do - it 'returns a Float' do - expect(described_class.monotonic_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.monotonic_time).to be_an_instance_of(Fixnum) end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 3b1c67a2147..f1a191d9410 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -46,19 +46,11 @@ describe Gitlab::Metrics::Transaction do end end - describe '#measure_method' do - it 'adds a new method if it does not exist already' do - transaction.measure_method('Foo#bar') { 'foo' } + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction.method_call_for('Foo#bar') - expect(transaction.methods['Foo#bar']). - to be_an_instance_of(Gitlab::Metrics::MethodCall) - end - - it 'adds timings to an existing method call' do - transaction.measure_method('Foo#bar') { 'foo' } - transaction.measure_method('Foo#bar') { 'foo' } - - expect(transaction.methods['Foo#bar'].call_count).to eq(2) + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 96f7eabbca6..84f9475a0f8 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -147,4 +147,10 @@ describe Gitlab::Metrics do end end end + + describe '#series_prefix' do + it 'returns a String' do + expect(described_class.series_prefix).to be_an_instance_of(String) + end + end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 1acb5846fcf..cd5f40fe3d2 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -1,6 +1,62 @@ require 'spec_helper' describe Ability, lib: true do + describe '.can_edit_note?' do + let(:project) { create(:empty_project) } + let!(:note) { create(:note_on_issue, project: project) } + + context 'using an anonymous user' do + it 'returns false' do + expect(described_class.can_edit_note?(nil, note)).to be_falsy + end + end + + context 'using a system note' do + it 'returns false' do + system_note = create(:note, system: true) + user = create(:user) + + expect(described_class.can_edit_note?(user, system_note)).to be_falsy + end + end + + context 'using users with different access levels' do + let(:user) { create(:user) } + + it 'returns true for the author' do + expect(described_class.can_edit_note?(note.author, note)).to be_truthy + end + + it 'returns false for a guest user' do + project.team << [user, :guest] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns false for a developer' do + project.team << [user, :developer] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns true for a master' do + project.team << [user, :master] + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + + it 'returns true for a group owner' do + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(user) + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + end + end + describe '.users_that_can_read_project' do context 'using a public project' do it 'returns all the users' do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 978ad9c52d5..dc88697199b 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -259,7 +259,7 @@ describe Ci::Build, models: true do let(:trigger) { create(:ci_trigger, project: project) } let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:user_trigger_variable) do - { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } end let(:predefined_trigger_variable) do { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 40181a8b906..44cd3c08718 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -79,6 +79,18 @@ describe Member, models: true do @accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request } end + describe '.access_for_user_ids' do + it 'returns the right access levels' do + users = [@owner_user.id, @master_user.id] + expected = { + @owner_user.id => Gitlab::Access::OWNER, + @master_user.id => Gitlab::Access::MASTER + } + + expect(described_class.access_for_user_ids(users)).to eq(expected) + end + end + describe '.invite' do it { expect(described_class.invite).not_to include @master } it { expect(described_class.invite).to include @invited_member } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 5f618322aab..62ae5f6cf74 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -340,18 +340,36 @@ describe HipchatService, models: true do end context "#message_options" do - it "should be set to the defaults" do - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' }) + it "is set to the defaults" do + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) end - it "should set notfiy to true" do + it "sets notify to true" do allow(hipchat).to receive(:notify).and_return('1') - expect(hipchat.send(:message_options)).to eq({ notify: true, color: 'yellow' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) end - it "should set the color" do + it "sets the color" do allow(hipchat).to receive(:color).and_return('red') - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'red' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) + end + + context 'with a successful build' do + it 'uses the green color' do + build_data = { object_kind: 'build', commit: { status: 'success' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' }) + end + end + + context 'with a failed build' do + it 'uses the red color' do + build_data = { object_kind: 'build', commit: { status: 'failed' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) + end end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9b017288488..72b8a4e25bd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -245,6 +245,34 @@ describe Project, models: true do end end + describe "#new_issue_address" do + let(:project) { create(:empty_project, path: "somewhere") } + let(:user) { create(:user) } + + context 'incoming email enabled' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + end + + it 'returns the address to create a new issue' do + token = user.authentication_token + address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab" + + expect(project.new_issue_address(user)).to eq(address) + end + end + + context 'incoming email disabled' do + before do + stub_incoming_email_setting(enabled: false) + end + + it 'returns nil' do + expect(project.new_issue_address(user)).to be_nil + end + end + end + describe 'last_activity methods' do let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } @@ -372,6 +400,24 @@ describe Project, models: true do it { expect(@project.to_param).to eq('gitlabhq') } end + + context 'with invalid path' do + it 'returns previous path to keep project suitable for use in URLs when persisted' do + project = create(:empty_project, path: 'gitlab') + project.path = 'foo&bar' + + expect(project).not_to be_valid + expect(project.to_param).to eq 'gitlab' + end + + it 'returns current path when new record' do + project = build(:empty_project, path: 'gitlab') + project.path = 'foo&bar' + + expect(project).not_to be_valid + expect(project.to_param).to eq 'foo&bar' + end + end end describe '#repository' do @@ -1244,6 +1290,32 @@ describe Project, models: true do end end + describe '#add_import_job' do + context 'forked' do + let(:forked_project_link) { create(:forked_project_link) } + let(:forked_from_project) { forked_project_link.forked_from_project } + let(:project) { forked_project_link.forked_to_project } + + it 'schedules a RepositoryForkWorker job' do + expect(RepositoryForkWorker).to receive(:perform_async). + with(project.id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, project.namespace.path) + + project.add_import_job + end + end + + context 'not forked' do + let(:project) { create(:project) } + + it 'schedules a RepositoryImportWorker job' do + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) + + project.add_import_job + end + end + end + describe '.where_paths_in' do context 'without any paths' do it 'returns an empty relation' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 9262aeb6ed8..1f42fbd3385 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -151,8 +151,8 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } end context 'when project is shared with group' do @@ -168,14 +168,14 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } context 'but share_with_group_lock is true' do before { project.namespace.update(share_with_group_lock: true) } - it { expect(project.team.max_member_access(master.id)).to be_nil } - it { expect(project.team.max_member_access(reporter.id)).to be_nil } + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) } end end end @@ -194,8 +194,53 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } + end + end + + describe "#max_member_access_for_users" do + it 'returns correct roles for different users' do + master = create(:user) + reporter = create(:user) + promoted_guest = create(:user) + guest = create(:user) + project = create(:project) + + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [promoted_guest, :guest] + project.team << [guest, :guest] + + group = create(:group) + group_developer = create(:user) + second_developer = create(:user) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) + + second_group = create(:group) + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER) + second_group.add_master(second_developer) + + users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + + expected = { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER + } + + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 881ab5ff8dc..5bc1bd9a930 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -446,6 +446,43 @@ describe Repository, models: true do end.to raise_error(GitHooksService::PreReceiveError) end end + + context 'when target branch is different from source branch' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires branch cache' do + expect(repository).not_to receive(:expire_exists_cache) + expect(repository).not_to receive(:expire_root_ref_cache) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).to receive(:expire_branches_cache) + expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branch_count_cache) + + repository.commit_with_hooks(user, 'new-feature') { sample_commit.id } + end + end + + context 'when repository is empty' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires creation and branch cache' do + empty_repository = create(:empty_project, :empty_repo).repository + + expect(empty_repository).to receive(:expire_exists_cache) + expect(empty_repository).to receive(:expire_root_ref_cache) + expect(empty_repository).to receive(:expire_emptiness_caches) + expect(empty_repository).to receive(:expire_branches_cache) + expect(empty_repository).to receive(:expire_has_visible_content_cache) + expect(empty_repository).to receive(:expire_branch_count_cache) + + empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + 'Updates file content', 'master', false) + end + end end describe '#exists?' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5219c808791..e4ea8506598 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -73,9 +73,13 @@ describe API::API, api: true do context "authorized user" do it "should return a commit by sha" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['id']).to eq(project.repository.commit.id) expect(json_response['title']).to eq(project.repository.commit.title) + expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) + expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) end it "should return a 404 error if not found" do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 12f2cfa6942..9d3d28e0b91 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -531,10 +531,8 @@ describe API::API, api: true do describe 'POST /projects/:id/issues with spam filtering' do before do - Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:check_for_spam?).and_return(true) - allow(endpoint).to receive(:is_spam?).and_return(true) - end + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) end let(:params) do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 1c7c60ec644..cf1e8d9b514 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -98,7 +98,7 @@ describe Ci::API::API do { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false } + { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } ) end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 8b19936ae6d..69eeb45ed71 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' -# team_update_admin_user PUT /admin/users/:id/team_update(.:format) admin/users#team_update # block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block # unblock_admin_user PUT /admin/users/:id/unblock(.:format) admin/users#unblock # admin_users GET /admin/users(.:format) admin/users#index @@ -11,10 +10,6 @@ require 'spec_helper' # PUT /admin/users/:id(.:format) admin/users#update # DELETE /admin/users/:id(.:format) admin/users#destroy describe Admin::UsersController, "routing" do - it "to #team_update" do - expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1') - end - it "to #block" do expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1') end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 620f328a114..9151cd3aefe 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -135,10 +135,6 @@ describe Projects::RepositoriesController, 'routing' do it 'to #archive format:tar.bz2' do expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq') - end end describe Projects::BranchesController, 'routing' do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 0a52c1ab933..1d4df9197f6 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -176,18 +176,10 @@ describe Profiles::KeysController, "routing" do expect(post("/profile/keys")).to route_to('profiles/keys#create') end - it "to #edit" do - expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') - end - it "to #show" do expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1') end - it "to #update" do - expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1') - end - it "to #destroy" do expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1') end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb new file mode 100644 index 00000000000..6f8f7109e14 --- /dev/null +++ b/spec/simplecov_env.rb @@ -0,0 +1,54 @@ +require 'simplecov' + +module SimpleCovEnv + extend self + + def start! + return unless ENV['SIMPLECOV'] + + configure_profile + configure_job + + SimpleCov.start + end + + def configure_job + SimpleCov.configure do + if ENV['CI_BUILD_NAME'] + coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}" + command_name ENV['CI_BUILD_NAME'] + end + + if ENV['CI'] + SimpleCov.at_exit do + # In CI environment don't generate formatted reports + # Only generate .resultset.json + SimpleCov.result + end + end + end + end + + def configure_profile + SimpleCov.configure do + load_profile 'test_frameworks' + track_files '{app,lib}/**/*.rb' + + add_filter '/vendor/ruby/' + add_filter 'config/initializers/' + + add_group 'Controllers', 'app/controllers' + add_group 'Models', 'app/models' + add_group 'Mailers', 'app/mailers' + add_group 'Helpers', 'app/helpers' + add_group 'Workers', %w(app/jobs app/workers) + add_group 'Libraries', 'lib' + add_group 'Services', 'app/services' + add_group 'Finders', 'app/finders' + add_group 'Uploaders', 'app/uploaders' + add_group 'Validators', 'app/validators' + + merge_timeout 7200 + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3638dcbb2d3..4f3aacf55be 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,5 @@ -if ENV['SIMPLECOV'] - require 'simplecov' - SimpleCov.start :rails -end +require './spec/simplecov_env' +SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 42220a20c75..464051063d8 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -44,9 +44,29 @@ describe 'projects/builds/show' do it 'shows commit title and not show commit message' do render - + expect(rendered).to have_css('p.build-light-text.append-bottom-0', text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) end end + + describe 'shows trigger variables in sidebar' do + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) } + + before do + build.trigger_request = trigger_request + render + end + + it 'shows trigger variables in separate lines' do + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1')) + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2')) + end + end + + private + + def variable_regexp(key, value) + /\A#{Regexp.escape("#{key}=#{value}")}\Z/ + end end diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index de40a6f78af..fe70501eeac 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -17,7 +17,7 @@ describe EmailReceiverWorker do context "when an error occurs" do before do - allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError) + allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError) end it "sends out a rejection email" do diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 5f762282b5e..60605460adb 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -14,21 +14,24 @@ describe RepositoryForkWorker do describe "#perform" do it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) @@ -38,7 +41,7 @@ describe RepositoryForkWorker do expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, project.path_with_namespace, + subject.perform(project.id, '/test/path', project.path_with_namespace, fork_project.namespace.path) end @@ -49,6 +52,7 @@ describe RepositoryForkWorker do subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml index 0b329aaf1c4..00f9541e89b 100644 --- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -2,7 +2,7 @@ # The image already has Hex installed. You might want to consider to use `elixir:latest` image: trenpixster/elixir:latest -# Pic zero or more services to be used on all builds. +# Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. # Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service services: