diff options
168 files changed, 2448 insertions, 1287 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f27d809af3c..766fb3a2ef7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-7.1-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6" .default-cache: &default-cache key: "ruby-233-with-yarn" @@ -191,6 +191,9 @@ review-docs-deploy: stage: build environment: name: review-docs/$CI_COMMIT_REF_NAME + # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables + # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 + url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - gem install gitlab --no-doc @@ -543,7 +546,7 @@ karma: <<: *dedicated-runner <<: *except-docs <<: *pull-cache - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-60.0-node-7.1-postgresql-9.6" + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6" stage: test variables: BABEL_ENV: "coverage" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f25171e8a6..dfb2ce0099a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -286,7 +286,10 @@ might be edited to make them small and simple. Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker. -For changes in the interface, it can be helpful to create a mockup first. +For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should +be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may +need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself. + If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. diff --git a/Gemfile.lock b/Gemfile.lock index bcbe6b4f394..e10db81d0c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -357,7 +357,7 @@ GEM rake grape_logging (1.7.0) grape - grpc (1.4.5) + grpc (1.6.0) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js index af8bcdc1794..cbc28374b80 100644 --- a/app/assets/javascripts/branches/branches_delete_modal.js +++ b/app/assets/javascripts/branches/branches_delete_modal.js @@ -7,6 +7,7 @@ class DeleteModal { this.$branchName = $('.js-branch-name', this.$modal); this.$confirmInput = $('.js-delete-branch-input', this.$modal); this.$deleteBtn = $('.js-delete-branch', this.$modal); + this.$notMerged = $('.js-not-merged', this.$modal); this.bindEvents(); } @@ -16,8 +17,10 @@ class DeleteModal { } setModalData(e) { - this.branchName = e.currentTarget.dataset.branchName || ''; - this.deletePath = e.currentTarget.dataset.deletePath || ''; + const branchData = e.currentTarget.dataset; + this.branchName = branchData.branchName || ''; + this.deletePath = branchData.deletePath || ''; + this.isMerged = !!branchData.isMerged; this.updateModal(); } @@ -30,6 +33,7 @@ class DeleteModal { this.$confirmInput.val(''); this.$deleteBtn.attr('href', this.deletePath); this.$deleteBtn.attr('disabled', true); + this.$notMerged.toggleClass('hidden', this.isMerged); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 7246ccbb281..720fbc87ea0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { params: { per_page: 20, active: true, + group_id: this.getGroupId(), project_id: this.getProjectId(), current_user: true, }, @@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown { super.renderContent(forceShowList); } + getGroupId() { + return this.input.getAttribute('data-group-id'); + } + getProjectId() { return this.input.getAttribute('data-project-id'); } diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index ad8254167a2..157280d66e3 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -77,10 +77,11 @@ export const hideMenu = (el) => { export const moveSubItemsToPosition = (el, subItems) => { const boundingRect = el.getBoundingClientRect(); const top = calculateTop(boundingRect, subItems.offsetHeight); + const left = sidebar ? sidebar.offsetWidth : 50; const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign + subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign const subItemsRect = subItems.getBoundingClientRect(); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 7d7f91227f9..2538d9c2093 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -127,13 +127,6 @@ import DropdownUtils from './filtered_search/dropdown_utils'; $('.has-tooltip', $value).tooltip({ container: 'body' }); - return $value.find('a').each(function(i) { - return setTimeout((function(_this) { - return function() { - return gl.animate.animate($(_this), 'pulse'); - }; - })(this), 200 * i); - }); }); }; $dropdown.glDropdown({ diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js deleted file mode 100644 index d93c1d0da59..00000000000 --- a/app/assets/javascripts/lib/utils/animate.js +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */ -(function() { - (function(w) { - if (w.gl == null) { - w.gl = {}; - } - if (gl.animate == null) { - gl.animate = {}; - } - gl.animate.animate = function($el, animation, options, done) { - if ((options != null ? options.cssStart : void 0) != null) { - $el.css(options.cssStart); - } - $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() { - $(this).removeClass(animation + ' animated'); - if (done != null) { - done(); - } - if ((options != null ? options.cssEnd : void 0) != null) { - $el.css(options.cssEnd); - } - }); - }; - gl.animate.animateEach = function($els, animation, time, options, done) { - var dfd; - dfd = $.Deferred(); - if (!$els.length) { - dfd.resolve(); - } - $els.each(function(i) { - setTimeout((function(_this) { - return function() { - var $this; - $this = $(_this); - return gl.animate.animate($this, animation, options, function() { - if (i === $els.length - 1) { - dfd.resolve(); - if (done != null) { - return done(); - } - } - }); - }; - })(this), time * i); - }); - return dfd.promise(); - }; - })(window); -}).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0bc31a56684..0f84470828a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -39,7 +39,6 @@ import './commit/file'; import './commit/image_file'; // lib/utils -import './lib/utils/animate'; import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/common_utils'; import './lib/utils/datetime_utility'; diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index cea4f35096a..f2eb2338a1e 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -15,7 +15,6 @@ export default class NewNavSidebar { this.$openSidebar = $('.toggle-mobile-nav'); this.$closeSidebar = $('.close-nav-button'); this.$sidebarToggle = $('.js-toggle-sidebar'); - this.$topLevelLinks = $('.sidebar-top-level-items > li > a'); } bindEvents() { @@ -56,10 +55,6 @@ export default class NewNavSidebar { this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); - - this.$topLevelLinks.attr('title', function updateTopLevelTitle() { - return collapsed ? this.getAttribute('aria-label') : ''; - }); } render() { diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue index 60c172321d1..feb3e73194b 100644 --- a/app/assets/javascripts/notes/components/issue_note_actions.vue +++ b/app/assets/javascripts/notes/components/issue_note_actions.vue @@ -86,7 +86,7 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role">{{accessLevel}}</span> + class="note-role note-role-access">{{accessLevel}}</span> <div v-if="canAddAwardEmoji" class="note-actions-item"> diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 5ffa67a1220..2f7717760ec 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -328,7 +328,7 @@ border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; - margin-top: $header-height; + margin-top: $new-navbar-height; .container-fluid { position: relative; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ef58382ba41..48dc25d343b 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -78,16 +78,16 @@ .right-sidebar { border-left: 1px solid $border-color; - height: calc(100% - #{$header-height}); + height: calc(100% - #{$new-navbar-height}); &.affix { position: fixed; - top: $header-height; + top: $new-navbar-height; } } .with-performance-bar .right-sidebar.affix { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } @mixin maintain-sidebar-dimensions { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 594b0bb0556..75cb9e009bb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -13,6 +13,7 @@ $sidebar-breakpoint: 1024px; $darken-normal-factor: 7%; $darken-dark-factor: 10%; $darken-border-factor: 5%; +$darken-border-dashed-factor: 25%; $white-light: #fff; $white-normal: #f0f0f0; @@ -134,6 +135,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor); $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); +$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 952002c83d1..8030854e527 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -105,7 +105,8 @@ $new-sidebar-collapsed-width: 50px; } &.sidebar-icons-only { - width: $new-sidebar-collapsed-width; + width: auto; + min-width: $new-sidebar-collapsed-width; .nav-sidebar-inner-scroll { overflow-x: hidden; @@ -124,6 +125,10 @@ $new-sidebar-collapsed-width: 50px; .fly-out-top-item { display: block; } + + .avatar-container { + margin-right: 0; + } } &.nav-sidebar-expanded { @@ -187,7 +192,7 @@ $new-sidebar-collapsed-width: 50px; .nav-sidebar-inner-scroll { height: 100%; width: 100%; - overflow: auto; + overflow: scroll; } .with-performance-bar .nav-sidebar { @@ -248,7 +253,7 @@ $new-sidebar-collapsed-width: 50px; @media (min-width: $screen-sm-min) { position: fixed; top: 0; - left: $new-sidebar-width; + left: 0; min-width: 150px; margin-top: -1px; padding: 4px 1px; @@ -386,10 +391,6 @@ $new-sidebar-collapsed-width: 50px; } .sidebar-sub-level-items { - @media (min-width: $screen-sm-min) { - left: $new-sidebar-collapsed-width; - } - &:not(.flyout-list) { display: none; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 50ec5110bf1..359dd388d05 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -64,10 +64,10 @@ color: $gl-text-color; position: sticky; position: -webkit-sticky; - top: $header-height; + top: $new-navbar-height; &.affix { - top: $header-height; + top: $new-navbar-height; } // with sidebar @@ -174,10 +174,10 @@ .with-performance-bar .build-page { .top-bar { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; &.affix { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 54c3c0173ae..951580ea1fe 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -634,8 +634,16 @@ padding-top: 8px; padding-bottom: 8px; } + + .diff-changed-file { + display: flex; + align-items: center; + } } .diff-file-changes-path { - @include str-truncated(78%); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index d4dc43035eb..cf5f933a762 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -95,6 +95,8 @@ } .omniauth-container { + font-size: 13px; + p { margin: 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 94e4f4334d4..6400b72742c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -752,7 +752,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-normal; + border: 1px dashed $border-gray-normal-dashed; border-radius: $border-radius-default; a { diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index e5cba774dcb..a7ab481519d 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(create_params.merge(user: current_user)) - - if @deploy_key.save + @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute + if @deploy_key.persisted? redirect_to admin_deploy_keys_path else render 'new' diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index dfc8bd0ba81..10e8e54f402 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -3,31 +3,10 @@ class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [:users, :award_emojis] before_action :load_project, only: [:users] - before_action :find_users, only: [:users] + before_action :load_group, only: [:users] def users - @users ||= User.none - @users = @users.active - @users = @users.reorder(:name) - @users = @users.search(params[:search]) if params[:search].present? - @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present? - @users = @users.page(params[:page]).per(params[:per_page]) - - if params[:todo_filter].present? && current_user - @users = @users.todo_authors(current_user.id, params[:todo_state_filter]) - end - - if params[:search].blank? - # Include current user if available to filter by "Me" - if params[:current_user].present? && current_user - @users = [current_user, *@users].uniq - end - - if params[:author_id].present? && current_user - author = User.find_by_id(params[:author_id]) - @users = [author, *@users].uniq if author - end - end + @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute render json: @users, only: [:name, :username, :id], methods: [:avatar_url] end @@ -60,26 +39,14 @@ class AutocompleteController < ApplicationController private - def find_users - @users = - if @project - user_ids = @project.team.users.pluck(:id) - - if params[:author_id].present? - user_ids << params[:author_id] - end - - User.where(id: user_ids) - elsif params[:group_id].present? + def load_group + @group ||= begin + if @project.blank? && params[:group_id].present? group = Group.find(params[:group_id]) return render_404 unless can?(current_user, :read_group, group) - - group.users - elsif current_user - User.all - else - User.none + group end + end end def load_project diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index 6779cc6ddac..689c76059f6 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end def create - @gpg_key = current_user.gpg_keys.new(gpg_key_params) + @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute - if @gpg_key.save + if @gpg_key.persisted? redirect_to profile_gpg_keys_path else @gpg_keys = current_user.gpg_keys.select(&:persisted?) diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index f9f0e8eef83..89d6d7f1b52 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController end def create - @key = current_user.keys.new(key_params) + @key = Keys::CreateService.new(current_user, key_params).execute - if @key.save + if @key.persisted? redirect_to profile_key_path(@key) else @keys = current_user.keys.select(&:persisted?) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 193549663ac..3c8eaa24080 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -27,7 +27,7 @@ class Projects::CompareController < Projects::ApplicationController def create if params[:from].blank? || params[:to].blank? - flash[:alert] = "You must select from and to branches" + flash[:alert] = "You must select a Source and a Target revision" from_to_vars = { from: params[:from].presence, to: params[:to].presence diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index c2e621fa190..cf8829ba95b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(create_params.merge(user: current_user)) + @key = DeployKeys::CreateService.new(current_user, create_params).execute unless @key.valid? && @project.deploy_keys << @key flash[:alert] = @key.errors.full_messages.join(', ').html_safe diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb new file mode 100644 index 00000000000..b8f52e31926 --- /dev/null +++ b/app/finders/autocomplete_users_finder.rb @@ -0,0 +1,60 @@ +class AutocompleteUsersFinder + attr_reader :current_user, :project, :group, :search, :skip_users, + :page, :per_page, :author_id, :params + + def initialize(params:, current_user:, project:, group:) + @current_user = current_user + @project = project + @group = group + @search = params[:search] + @skip_users = params[:skip_users] + @page = params[:page] + @per_page = params[:per_page] + @author_id = params[:author_id] + @params = params + end + + def execute + items = find_users + items = items.active + items = items.reorder(:name) + items = items.search(search) if search.present? + items = items.where.not(id: skip_users) if skip_users.present? + items = items.page(page).per(per_page) + + if params[:todo_filter].present? && current_user + items = items.todo_authors(current_user.id, params[:todo_state_filter]) + end + + if search.blank? + # Include current user if available to filter by "Me" + if params[:current_user].present? && current_user + items = [current_user, *items].uniq + end + + if author_id.present? && current_user + author = User.find_by_id(author_id) + items = [author, *items].uniq if author + end + end + + items + end + + private + + def find_users + return users_from_project if project + return group.users if group + return User.all if current_user + + User.none + end + + def users_from_project + user_ids = project.team.users.pluck(:id) + user_ids << author_id if author_id.present? + + User.where(id: user_ids) + end +end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 3308ab0c259..ee701076a14 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -119,8 +119,4 @@ module TabHelper 'active' if current_controller?('oauth/applications') end - - def sidebar_link(href, title: nil, css: nil, &block) - link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title } - end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 476db384bbd..8d017b9b3b1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -453,6 +453,10 @@ module Ci .fabricate! end + def latest_builds_with_artifacts + @latest_builds_with_artifacts ||= builds.latest.with_artifacts + end + private def ci_yaml_from_repo diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 51768dd96bc..eae5eee4fee 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -28,10 +28,4 @@ class DeployKey < Key def can_push_to?(project) can_push? && has_access_to?(project) end - - private - - # we don't want to notify the user for deploy keys - def notify_user - end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 9b05f8b1cd5..44e39e21442 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,7 +6,10 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true, validate: true - has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, + -> (env) { where(project_id: env.project_id) }, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 1633acd4fa9..44deae4234b 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base before_validation :extract_fingerprint, :extract_primary_keyid after_commit :update_invalid_gpg_signatures, on: :create - after_commit :notify_user, on: :create def primary_keyid super&.upcase @@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base # only allows one key self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first end - - def notify_user - NotificationService.new.new_gpg_key(self) - end end diff --git a/app/models/key.rb b/app/models/key.rb index a6b4dcfec0d..4fa6cac2fd0 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -28,7 +28,6 @@ class Key < ActiveRecord::Base delegate :name, :email, to: :user, prefix: true after_commit :add_to_shell, on: :create - after_commit :notify_user, on: :create after_create :post_create_hook after_commit :remove_from_shell, on: :destroy after_destroy :post_destroy_hook @@ -118,8 +117,4 @@ class Key < ActiveRecord::Base "type is forbidden. Must be #{allowed_types}" end - - def notify_user - NotificationService.new.new_key(self) - end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4a9a23fea1f..e279d8dd8c5 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base end def force_share_with_group_lock_on_descendants - descendants.update_all(share_with_group_lock: true) + return unless Group.supports_nested_groups? + + # We can't use `descendants.update_all` since Rails will throw away the WITH + # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use + # different table aliases, hence we're just using WHERE IN. Since we have a + # maximum of 20 nested groups this should be fine. + Namespace.where(id: descendants.select(:id)) + .update_all(share_with_group_lock: true) end end diff --git a/app/models/project.rb b/app/models/project.rb index ff5638dd155..94ae0acbe1a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -161,7 +161,7 @@ class Project < ActiveRecord::Base has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true - has_one :project_feature + has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' # Container repositories need to remove data from the container registry, @@ -190,7 +190,7 @@ class Project < ActiveRecord::Base has_one :auto_devops, class_name: 'ProjectAutoDevops' accepts_nested_attributes_for :variables, allow_destroy: true - accepts_nested_attributes_for :project_feature + accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data accepts_nested_attributes_for :auto_devops @@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end + def latest_successful_pipeline_for_default_branch + if defined?(@latest_successful_pipeline_for_default_branch) + return @latest_successful_pipeline_for_default_branch + end + + @latest_successful_pipeline_for_default_branch = + pipelines.latest_successful_for(default_branch) + end + + def latest_successful_pipeline_for(ref = nil) + if ref && ref != default_branch + pipelines.latest_successful_for(ref) + else + latest_successful_pipeline_for_default_branch + end + end + def enable_ci project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index fb1db0255aa..bfb8d703ec9 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to belongs_to :project, -> { unscope(where: :pending_delete) } + validates :project, presence: true + validate :repository_children_level default_value_for :builds_access_level, value: ENABLED, allows_nil: false diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 9d37184be2c..6a3118a11b8 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -80,6 +80,6 @@ class PipelinesEmailService < Service end def retrieve_recipients(data) - recipients.to_s.split(',').reject(&:blank?) + recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 035f85a0b46..af9911ea045 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -90,6 +90,12 @@ class Repository ) end + # we need to have this method here because it is not cached in ::Git and + # the method is called multiple times for every request + def has_visible_content? + branch_count > 0 + end + def inspect "#<#{self.class.name}:#{@disk_path}>" end @@ -166,7 +172,7 @@ class Repository end def add_branch(user, branch_name, ref) - branch = raw_repository.add_branch(branch_name, committer: user, target: ref) + branch = raw_repository.add_branch(branch_name, user: user, target: ref) after_create_branch @@ -176,7 +182,7 @@ class Repository end def add_tag(user, tag_name, target, message = nil) - raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + raw_repository.add_tag(tag_name, user: user, target: target, message: message) rescue Gitlab::Git::Repository::InvalidRef false end @@ -184,7 +190,7 @@ class Repository def rm_branch(user, branch_name) before_remove_branch - raw_repository.rm_branch(branch_name, committer: user) + raw_repository.rm_branch(branch_name, user: user) after_remove_branch true @@ -193,7 +199,7 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - raw_repository.rm_tag(tag_name, committer: user) + raw_repository.rm_tag(tag_name, user: user) after_remove_tag true @@ -762,17 +768,23 @@ class Repository multi_action(**options) end - def with_branch(user, *args) - result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| - yield start_commit - end + def with_cache_hooks + result = yield + + return unless result - newrev, should_run_after_create, should_run_after_create_branch = result + after_create if result.repo_created? + after_create_branch if result.branch_created? - after_create if should_run_after_create - after_create_branch if should_run_after_create_branch + result.newrev + end - newrev + def with_branch(user, *args) + with_cache_hooks do + Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| + yield start_commit + end + end end # rubocop:disable Metrics/ParameterLists @@ -837,30 +849,13 @@ class Repository end end - def merge(user, source, merge_request, options = {}) - with_branch( - user, - merge_request.target_branch) do |start_commit| - our_commit = start_commit.sha - their_commit = source - - raise 'Invalid merge target' unless our_commit - raise 'Invalid merge source' unless their_commit - - merge_index = rugged.merge_commits(our_commit, their_commit) - break if merge_index.conflicts? - - actual_options = options.merge( - parents: [our_commit, their_commit], - tree: merge_index.write_tree(rugged) - ) - - commit_id = create_commit(actual_options) - merge_request.update(in_progress_merge_commit_sha: commit_id) - commit_id + def merge(user, source_sha, merge_request, message) + with_cache_hooks do + raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id| + merge_request.update(in_progress_merge_commit_sha: commit_id) + nil # Return value does not matter. + end end - rescue Gitlab::Git::CommitError # when merge_index.conflicts? - false end def revert( @@ -1151,12 +1146,6 @@ class Repository Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) end - def create_commit(params = {}) - params[:message].delete!("\r") - - Rugged::Commit.create(rugged, params) - end - def last_commit_for_path_by_gitaly(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) commit(c) diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb new file mode 100644 index 00000000000..16de3d08df2 --- /dev/null +++ b/app/services/deploy_keys/create_service.rb @@ -0,0 +1,7 @@ +module DeployKeys + class CreateService < Keys::BaseService + def execute + DeployKey.create(params.merge(user: user)) + end + end +end diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb new file mode 100644 index 00000000000..e822a89c4d3 --- /dev/null +++ b/app/services/gpg_keys/create_service.rb @@ -0,0 +1,9 @@ +module GpgKeys + class CreateService < Keys::BaseService + def execute + key = user.gpg_keys.create(params) + notification_service.new_gpg_key(key) if key.persisted? + key + end + end +end diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb new file mode 100644 index 00000000000..545832d0bd4 --- /dev/null +++ b/app/services/keys/base_service.rb @@ -0,0 +1,13 @@ +module Keys + class BaseService + attr_accessor :user, :params + + def initialize(user, params) + @user, @params = user, params + end + + def notification_service + NotificationService.new + end + end +end diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb new file mode 100644 index 00000000000..e2e5a6c46c5 --- /dev/null +++ b/app/services/keys/create_service.rb @@ -0,0 +1,9 @@ +module Keys + class CreateService < ::Keys::BaseService + def execute + key = user.keys.create(params) + notification_service.new_key(key) if key.persisted? + key + end + end +end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index b2b6c5627fb..07cbd8f92a9 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -38,15 +38,9 @@ module MergeRequests private def commit - committer = repository.user_to_committer(current_user) + message = params[:commit_message] || merge_request.merge_commit_message - options = { - message: params[:commit_message] || merge_request.merge_commit_message, - author: committer, - committer: committer - } - - commit_id = repository.merge(current_user, source, merge_request, options) + commit_id = repository.merge(current_user, source, merge_request, message) raise MergeError, 'Conflicts detected during merge' unless commit_id diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index cb4ffcab778..13e292a18bf 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -24,7 +24,10 @@ module Projects success else - error('Project could not be updated!') + model_errors = project.errors.full_messages.to_sentence + error_message = model_errors.presence || 'Project could not be updated!' + + error(error_message) end end diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index bfd7dd25a7d..546cec4d565 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -7,6 +7,8 @@ %span.light - has_icon = provider_has_icon?(provider) = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - %fieldset.prepend-top-10 - = check_box_tag :remember_me - = label_tag :remember_me, 'Remember me' + %fieldset.prepend-top-10.checkbox.remember-me + %label + = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + %span + Remember me diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index fcebb385a65..615238b94ad 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -7,7 +7,7 @@ .sidebar-context-title Admin Area %ul.sidebar-top-level-items = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do - = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do + = link_to admin_root_path, class: 'shortcuts-tree' do .nav-icon-container = custom_icon('overview') %span.nav-item-name @@ -53,7 +53,7 @@ ConvDev Index = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do - = sidebar_link admin_system_info_path, title: _('Monitoring') do + = link_to admin_system_info_path do .nav-icon-container = custom_icon('monitoring') %span.nav-item-name @@ -87,7 +87,7 @@ Requests Profiles = nav_link(controller: :broadcast_messages) do - = sidebar_link admin_broadcast_messages_path, title: _('Messages') do + = link_to admin_broadcast_messages_path do .nav-icon-container = custom_icon('messages') %span.nav-item-name @@ -99,7 +99,7 @@ #{ _('Messages') } = nav_link(controller: [:hooks, :hook_logs]) do - = sidebar_link admin_hooks_path, title: _('Hooks') do + = link_to admin_hooks_path do .nav-icon-container = custom_icon('system_hooks') %span.nav-item-name @@ -111,7 +111,7 @@ #{ _('System Hooks') } = nav_link(controller: :applications) do - = sidebar_link admin_applications_path, title: _('Applications') do + = link_to admin_applications_path do .nav-icon-container = custom_icon('applications') %span.nav-item-name @@ -123,7 +123,7 @@ #{ _('Applications') } = nav_link(controller: :abuse_reports) do - = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do + = link_to admin_abuse_reports_path do .nav-icon-container = custom_icon('abuse_reports') %span.nav-item-name @@ -138,7 +138,7 @@ - if akismet_enabled? = nav_link(controller: :spam_logs) do - = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do + = link_to admin_spam_logs_path do .nav-icon-container = custom_icon('spam_logs') %span.nav-item-name @@ -150,7 +150,7 @@ #{ _('Spam Logs') } = nav_link(controller: :deploy_keys) do - = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do + = link_to admin_deploy_keys_path do .nav-icon-container = custom_icon('key') %span.nav-item-name @@ -162,7 +162,7 @@ #{ _('Deploy Keys') } = nav_link(controller: :services) do - = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do + = link_to admin_application_settings_services_path do .nav-icon-container = custom_icon('service_templates') %span.nav-item-name @@ -174,7 +174,7 @@ #{ _('Service Templates') } = nav_link(controller: :labels) do - = sidebar_link admin_labels_path, title: _('Labels') do + = link_to admin_labels_path do .nav-icon-container = custom_icon('labels') %span.nav-item-name @@ -186,7 +186,7 @@ #{ _('Labels') } = nav_link(controller: :appearances) do - = sidebar_link admin_appearances_path, title: _('Appearances') do + = link_to admin_appearances_path do .nav-icon-container = custom_icon('appearance') %span.nav-item-name @@ -198,7 +198,7 @@ #{ _('Appearance') } = nav_link(controller: :application_settings) do - = sidebar_link admin_application_settings_path, title: _('Settings') do + = link_to admin_application_settings_path do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index e01dfa7c854..cb44c012f56 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -11,7 +11,7 @@ = @group.name %ul.sidebar-top-level-items = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = sidebar_link group_path(@group), title: _('Group overview') do + = link_to group_path(@group) do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -34,7 +34,7 @@ Activity = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = sidebar_link issues_group_path(@group), title: _('Issues') do + = link_to issues_group_path(@group) do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -64,7 +64,7 @@ Milestones = nav_link(path: 'groups#merge_requests') do - = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do + = link_to merge_requests_group_path(@group) do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -77,19 +77,19 @@ #{ _('Merge Requests') } %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count) = nav_link(path: 'group_members#index') do - = sidebar_link group_group_members_path(@group), title: _('Members') do + = link_to group_group_members_path(@group) do .nav-icon-container = custom_icon('members') %span.nav-item-name Members %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do - = link_to merge_requests_group_path(@group) do + = link_to group_group_members_path(@group) do %strong.fly-out-top-item-name #{ _('Members') } - if current_user && can?(current_user, :admin_group, @group) = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = sidebar_link edit_group_path(@group), title: _('Settings') do + = link_to edit_group_path(@group) do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 4c26d107ea7..2c402591f62 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -7,7 +7,7 @@ .sidebar-context-title User Settings %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = sidebar_link profile_path, title: _('Profile Settings') do + = link_to profile_path do .nav-icon-container = custom_icon('profile') %span.nav-item-name @@ -18,7 +18,7 @@ %strong.fly-out-top-item-name #{ _('Profile') } = nav_link(controller: [:accounts, :two_factor_auths]) do - = sidebar_link profile_account_path, title: _('Account') do + = link_to profile_account_path do .nav-icon-container = custom_icon('account') %span.nav-item-name @@ -30,7 +30,7 @@ #{ _('Account') } - if current_application_settings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do - = sidebar_link applications_profile_path, title: _('Applications') do + = link_to applications_profile_path do .nav-icon-container = custom_icon('applications') %span.nav-item-name @@ -41,7 +41,7 @@ %strong.fly-out-top-item-name #{ _('Applications') } = nav_link(controller: :chat_names) do - = sidebar_link profile_chat_names_path, title: _('Chat') do + = link_to profile_chat_names_path do .nav-icon-container = custom_icon('chat') %span.nav-item-name @@ -52,7 +52,7 @@ %strong.fly-out-top-item-name #{ _('Chat') } = nav_link(controller: :personal_access_tokens) do - = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do + = link_to profile_personal_access_tokens_path do .nav-icon-container = custom_icon('access_tokens') %span.nav-item-name @@ -63,7 +63,7 @@ %strong.fly-out-top-item-name #{ _('Access Tokens') } = nav_link(controller: :emails) do - = sidebar_link profile_emails_path, title: _('Emails') do + = link_to profile_emails_path do .nav-icon-container = custom_icon('emails') %span.nav-item-name @@ -75,7 +75,7 @@ #{ _('Emails') } - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = sidebar_link edit_profile_password_path, title: _('Password') do + = link_to edit_profile_password_path do .nav-icon-container = custom_icon('lock') %span.nav-item-name @@ -86,7 +86,7 @@ %strong.fly-out-top-item-name #{ _('Password') } = nav_link(controller: :notifications) do - = sidebar_link profile_notifications_path, title: _('Notifications') do + = link_to profile_notifications_path do .nav-icon-container = custom_icon('notifications') %span.nav-item-name @@ -97,7 +97,7 @@ %strong.fly-out-top-item-name #{ _('Notifications') } = nav_link(controller: :keys) do - = sidebar_link profile_keys_path, title: _('SSH Keys') do + = link_to profile_keys_path do .nav-icon-container = custom_icon('key') %span.nav-item-name @@ -108,7 +108,7 @@ %strong.fly-out-top-item-name #{ _('SSH Keys') } = nav_link(controller: :gpg_keys) do - = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do + = link_to profile_gpg_keys_path do .nav-icon-container = custom_icon('key_2') %span.nav-item-name @@ -119,7 +119,7 @@ %strong.fly-out-top-item-name #{ _('GPG Keys') } = nav_link(controller: :preferences) do - = sidebar_link profile_preferences_path, title: _('Preferences') do + = link_to profile_preferences_path do .nav-icon-container = custom_icon('preferences') %span.nav-item-name @@ -130,7 +130,7 @@ %strong.fly-out-top-item-name #{ _('Preferences') } = nav_link(path: 'profiles#audit_log') do - = sidebar_link audit_log_profile_path, title: _('Authentication log') do + = link_to audit_log_profile_path do .nav-icon-container = custom_icon('authentication_log') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 5f7a2d86c0f..29f1fc6b354 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -9,7 +9,7 @@ = @project.name %ul.sidebar-top-level-items = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do + = link_to project_path(@project), class: 'shortcuts-project' do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -36,7 +36,7 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do + = link_to project_tree_path(@project), class: 'shortcuts-tree' do .nav-icon-container = custom_icon('doc_text') %span.nav-item-name @@ -82,7 +82,7 @@ - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do - = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do + = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do .nav-icon-container = custom_icon('container_registry') %span.nav-item-name @@ -90,7 +90,7 @@ - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do + = link_to project_issues_path(@project), class: 'shortcuts-issues' do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -144,7 +144,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do + = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -161,7 +161,7 @@ - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do + = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container = custom_icon('pipeline') %span.nav-item-name @@ -205,7 +205,7 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do + = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do .nav-icon-container = custom_icon('wiki') %span.nav-item-name @@ -218,7 +218,7 @@ - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do + = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do .nav-icon-container = custom_icon('snippets') %span.nav-item-name @@ -231,7 +231,7 @@ - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do + = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 19712a8f1be..05c1d2b383c 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -43,7 +43,8 @@ data: { toggle: "modal", target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), - branch_name: branch.name } } + branch_name: branch.name, + is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } = icon("trash-o") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml index c5888afa54d..f00a0ee6925 100644 --- a/app/views/projects/branches/_delete_protected_modal.html.haml +++ b/app/views/projects/branches/_delete_protected_modal.html.haml @@ -6,13 +6,18 @@ %h3.page-title Delete protected branch = surround "'", "'?" do - %span.js-branch-name>[branch name] + %span.js-branch-name.ref-name>[branch name] .modal-body %p You’re about to permanently delete the protected branch = succeed '.' do - %strong.js-branch-name [branch name] + %strong.js-branch-name.ref-name [branch name] + %p.js-not-merged + - default_branch = capture do + %span.ref-name= @repository.root_ref + = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch } + = s_("Branches|To avoid data loss, consider merging this branch before deleting it.") %p Once you confirm and press = succeed ',' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 883922dbf04..9d85e027ac9 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,4 @@ -- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) } +- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) } - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> @@ -26,18 +26,16 @@ %i.fa.fa-download %span= _('Download tar') - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do - %i.fa.fa-download - %span - #{ s_('DownloadArtifacts|Download') } '#{job.name}' + - if pipeline && pipeline.latest_builds_with_artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - pipeline.latest_builds_with_artifacts.each do |job| + %li + = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do + %i.fa.fa-download + %span + #{s_('DownloadArtifacts|Download')} '#{job.name}' diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 94b7db5eb25..a518fced2b4 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,22 +2,22 @@ .clearfix - if params[:to] && params[:from] .compare-switch-container - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'} - .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown - .input-group.inline-input-group - %span.input-group-addon from - = hidden_field_tag :from, params[:from] - = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' - = render 'shared/ref_dropdown' - .compare-ellipsis.inline ... + = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions' .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group - %span.input-group-addon to + %span.input-group-addon Source = hidden_field_tag :to, params[:to] = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' = render 'shared/ref_dropdown' + .compare-ellipsis.inline ... + .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown + .input-group.inline-input-group + %span.input-group-addon Target + = hidden_field_tag :from, params[:from] + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' + = render 'shared/ref_dropdown' = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if @merge_request.present? diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 2632fea6eba..1ce3ad0c0fd 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -7,13 +7,19 @@ .sub-header-block Compare Git revisions. %br - Fill input field with commit SHA like - %code.ref-name 4eedf23 - or branch/tag name like - %code.ref-name master - and press compare button for the commits list and a code diff. + Choose a branch/tag (e.g. + = succeed ')' do + %code.ref-name master + or enter a commit SHA (e.g. + = succeed ')' do + %code.ref-name 4eedf23 + to see what's changed or to create a merge request. %br - Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field. + Changes are shown as if the + %b source + revision was being merged into the + %b target + revision. .prepend-top-20 = render "form" diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index ad2d355ab4a..2de2cf9e38c 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -21,9 +21,9 @@ %ul - diff_files.each do |diff_file| %li - %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } + %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") - %span.diff-file-changes-path= diff_file.new_path + %span.diff-file-changes-path.append-right-5= diff_file.new_path .pull-right %span.cgreen< +#{diff_file.added_lines} diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index de85615c672..e660fce652f 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 .checkbox = f.check_box :access_level, {}, 'ref_protected', 'not_protected' - %span.light This runner will only run on pipelines trigged on protected branches + %span.light This runner will only run on pipelines triggered on protected branches .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index c4ed7f6e750..d3f0aa2d339 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -11,13 +11,13 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml new file mode 100644 index 00000000000..245b8129de8 --- /dev/null +++ b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml @@ -0,0 +1,5 @@ +--- +title: Return only group's members in user dropdowns on issuables list pages +merge_request: 14249 +author: +type: changed diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml new file mode 100644 index 00000000000..469cc04930b --- /dev/null +++ b/changelogs/unreleased/21331-improve-confusing-compare-page.yml @@ -0,0 +1,5 @@ +--- +title: Make the labels in the Compare form less confusing +merge_request: 14225 +author: +type: changed diff --git a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml new file mode 100644 index 00000000000..e43e97303e2 --- /dev/null +++ b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml @@ -0,0 +1,5 @@ +--- +title: Extract AutocompleteController#users into finder +merge_request: 13778 +author: Maxim Rydkin, Mayra Cabrera +type: other diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml new file mode 100644 index 00000000000..e7cad5a11d5 --- /dev/null +++ b/changelogs/unreleased/35917_create_services_for_keys.yml @@ -0,0 +1,4 @@ +--- +title: creation of keys moved to services +merge_request: 13331 +author: haseebeqx diff --git a/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml new file mode 100644 index 00000000000..6894e650c11 --- /dev/null +++ b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml @@ -0,0 +1,5 @@ +--- +title: Allow using newlines in pipeline email service recipients +merge_request: 14250 +author: +type: fixed diff --git a/changelogs/unreleased/change-dashed-border-button-color.yml b/changelogs/unreleased/change-dashed-border-button-color.yml new file mode 100644 index 00000000000..038bea79273 --- /dev/null +++ b/changelogs/unreleased/change-dashed-border-button-color.yml @@ -0,0 +1,5 @@ +--- +title: changed dashed border button color to be darker +merge_request: !14041 +author: +type: other diff --git a/changelogs/unreleased/ci-environment-status-performance.yml b/changelogs/unreleased/ci-environment-status-performance.yml new file mode 100644 index 00000000000..8812733b5a7 --- /dev/null +++ b/changelogs/unreleased/ci-environment-status-performance.yml @@ -0,0 +1,5 @@ +--- +title: Constrain environment deployments to project IDs +merge_request: +author: +type: other diff --git a/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml new file mode 100644 index 00000000000..f4a956e6724 --- /dev/null +++ b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml @@ -0,0 +1,5 @@ +--- +title: "Disallow NULL values for environments.project_id" +merge_request: +author: +type: other diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml new file mode 100644 index 00000000000..e0b3851b97f --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml @@ -0,0 +1,5 @@ +--- +title: Fixed the sidebar scrollbar overlapping links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/issue_37640.yml b/changelogs/unreleased/issue_37640.yml new file mode 100644 index 00000000000..d806ed64bed --- /dev/null +++ b/changelogs/unreleased/issue_37640.yml @@ -0,0 +1,6 @@ +--- +title: Fix project feature being deleted when updating project with invalid visibility + level +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml new file mode 100644 index 00000000000..5a7cd42b888 --- /dev/null +++ b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: "Memoize the latest builds of a pipeline on a project's homepage" +merge_request: +author: +type: other diff --git a/changelogs/unreleased/projects-controller-show.yml b/changelogs/unreleased/projects-controller-show.yml new file mode 100644 index 00000000000..25f4a72710b --- /dev/null +++ b/changelogs/unreleased/projects-controller-show.yml @@ -0,0 +1,5 @@ +--- +title: Memoize pipelines for project download buttons +merge_request: +author: +type: other diff --git a/changelogs/unreleased/refactor-animate-js.yml b/changelogs/unreleased/refactor-animate-js.yml new file mode 100644 index 00000000000..ec32d68bbdd --- /dev/null +++ b/changelogs/unreleased/refactor-animate-js.yml @@ -0,0 +1,5 @@ +--- +title: Remove animate.js and label animation. +merge_request: +author: +type: removed diff --git a/changelogs/unreleased/replace_project_archived-feature.yml b/changelogs/unreleased/replace_project_archived-feature.yml new file mode 100644 index 00000000000..d0697347aa0 --- /dev/null +++ b/changelogs/unreleased/replace_project_archived-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/archived.feature' spinach test with an rspec analog +merge_request: 14322 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_commits_revert-feature.yml b/changelogs/unreleased/replace_project_commits_revert-feature.yml new file mode 100644 index 00000000000..7fc9fcf3580 --- /dev/null +++ b/changelogs/unreleased/replace_project_commits_revert-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/commits/revert.feature' spinach test with an rspec analog +merge_request: 14325 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_search-feature.yml b/changelogs/unreleased/replace_search-feature.yml new file mode 100644 index 00000000000..487f602ba30 --- /dev/null +++ b/changelogs/unreleased/replace_search-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'search.feature' spinach test with an rspec analog +merge_request: 14248 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml new file mode 100644 index 00000000000..34aa3d0db6f --- /dev/null +++ b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml @@ -0,0 +1,5 @@ +--- +title: Made the "remember me" check boxes have consistent styles and alignment +merge_request: +author: Jedidiah Broadbent +type: fixed diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml new file mode 100644 index 00000000000..63f1f424a5d --- /dev/null +++ b/changelogs/unreleased/winh-protected-branch-modal-merged.yml @@ -0,0 +1,5 @@ +--- +title: Display whether branch has been merged when deleting protected branch +merge_request: 14220 +author: +type: changed diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index d6c3c84851b..6c5c8cad270 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -416,3 +416,39 @@ :why: https://gitlab.com/gitlab-com/organization/issues/117 :versions: [] :when: 2017-09-04 12:59:51.150798717 Z +- - :approve + - console-browserify + - :who: Mike Greiling + :why: https://github.com/Raynos/console-browserify/blob/f0a8898487e2a47b8a5dc8734b91059fa2825506/LICENCE + :versions: [] + :when: 2017-09-16 05:13:07.073651000 Z +- - :approve + - duplexer + - :who: Mike Greiling + :why: https://github.com/Raynos/duplexer/blob/master/LICENCE + :versions: [] + :when: 2017-09-16 05:14:15.774643000 Z +- - :approve + - json3 + - :who: Mike Greiling + :why: https://github.com/bestiejs/json3/blob/v3.3.2/LICENSE + :versions: [] + :when: 2017-09-16 05:15:16.273892000 Z +- - :approve + - mime + - :who: Mike Greiling + :why: https://github.com/broofa/node-mime/blob/v1.3.4/LICENSE + :versions: [] + :when: 2017-09-16 05:16:21.135542000 Z +- - :approve + - querystring-es3 + - :who: Mike Greiling + :why: https://github.com/mike-spainhower/querystring/blob/v0.2.0/License.md + :versions: [] + :when: 2017-09-16 05:17:20.372089000 Z +- - :approve + - utils-merge + - :who: Mike Greiling + :why: https://github.com/jaredhanson/utils-merge/blob/v1.0.0/LICENSE + :versions: [] + :when: 2017-09-16 05:18:26.193764000 Z diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index 943e01f1496..d3a7a2b9f8b 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -30,7 +30,7 @@ if app.config.serve_static_files settings.merge!( host: Gitlab.config.gitlab.host, port: Gitlab.config.gitlab.port, - https: Gitlab.config.gitlab.https + https: false ) app.config.middleware.insert_before( Gitlab::Middleware::Static, diff --git a/db/migrate/20170913131410_environments_project_id_not_null.rb b/db/migrate/20170913131410_environments_project_id_not_null.rb new file mode 100644 index 00000000000..d5404f8ede9 --- /dev/null +++ b/db/migrate/20170913131410_environments_project_id_not_null.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class EnvironmentsProjectIdNotNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_null :environments, :project_id, false + end + + def down + change_column_null :environments, :project_id, true + end +end diff --git a/db/migrate/20170914135630_add_index_for_recent_push_events.rb b/db/migrate/20170914135630_add_index_for_recent_push_events.rb new file mode 100644 index 00000000000..99f593b0465 --- /dev/null +++ b/db/migrate/20170914135630_add_index_for_recent_push_events.rb @@ -0,0 +1,40 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexForRecentPushEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index_if_not_present( + :merge_requests, + [:source_project_id, :source_branch] + ) + + remove_concurrent_index_if_present(:merge_requests, :source_project_id) + end + + def down + add_concurrent_index_if_not_present(:merge_requests, :source_project_id) + + remove_concurrent_index_if_present( + :merge_requests, + [:source_project_id, :source_branch] + ) + end + + def add_concurrent_index_if_not_present(table, columns) + return if index_exists?(table, columns) + + add_concurrent_index(table, columns) + end + + def remove_concurrent_index_if_present(table, columns) + return unless index_exists?(table, columns) + + remove_concurrent_index(table, columns) + end +end diff --git a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb new file mode 100644 index 00000000000..3e84b295be4 --- /dev/null +++ b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb @@ -0,0 +1,37 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class DeleteConflictingRedirectRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze + BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s. + DELAY_INTERVAL = 12.seconds + + disable_ddl_transaction! + + class Route < ActiveRecord::Base + include EachBatch + + self.table_name = 'routes' + end + + def up + say opening_message + + queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # nothing + end + + def opening_message + <<~MSG + Clean up redirect routes that conflict with regular routes. + See initial bug fix: + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357 + MSG + end +end diff --git a/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb new file mode 100644 index 00000000000..bfa9ad80c7d --- /dev/null +++ b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb @@ -0,0 +1,33 @@ +class FixProjectsWithoutProjectFeature < ActiveRecord::Migration + DOWNTIME = false + + def up + # Deletes corrupted project features + sql = "DELETE FROM project_features WHERE project_id IS NULL" + execute(sql) + + # Creates missing project features with private visibility + sql = + %Q{ + INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level, + builds_access_level, snippets_access_level, created_at, updated_at) + SELECT projects.id as project_id, + 10 as repository_access_level, + 10 as issues_access_level, + 10 as merge_requests_access_level, + 10 as wiki_access_level, + 10 as builds_access_level , + 10 as snippets_access_level, + projects.created_at, + projects.updated_at + FROM projects + LEFT OUTER JOIN project_features ON project_features.project_id = projects.id + WHERE (project_features.id IS NULL) + } + + execute(sql) + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index ceaa7a7deea..779b85f7a66 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170905112933) do +ActiveRecord::Schema.define(version: 20170914135630) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -520,7 +520,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree create_table "environments", force: :cascade do |t| - t.integer "project_id" + t.integer "project_id", null: false t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" @@ -894,7 +894,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree - add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree + add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index e99a7ee29cc..1304476e678 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -77,6 +77,33 @@ and use [an application password](https://support.google.com/mail/answer/185833) To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the [Postfix setup documentation](reply_by_email_postfix_setup.md). +### Security Concerns + +**WARNING:** Be careful when choosing the domain used for receiving incoming +email. + +For the sake of example, suppose your top-level company domain is `hooli.com`. +All employees in your company have an email address at that domain via Google +Apps, and your company's private Slack instance requires a valid `@hooli.com` +email address in order to sign up. + +If you also host a public-facing GitLab instance at `hooli.com` and set your +incoming email domain to `hooli.com`, an attacker could abuse the "Create new +issue by email" feature by using a project's unique address as the email when +signing up for Slack, which would send a confirmation email, which would create +a new issue on the project owned by the attacker, allowing them to click the +confirmation link and validate their account on your company's private Slack +instance. + +We recommend receiving incoming email on a subdomain, such as +`incoming.hooli.com`, and ensuring that you do not employ any services that +authenticate solely based on access to an email domain such as `*.hooli.com.` +Alternatively, use a dedicated domain for GitLab email communications such as +`hooli-gitlab.com`. + +See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366) +for a real-world example of this exploit. + ### Omnibus package installations 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the @@ -141,7 +168,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. gitlab_rails['incoming_email_idle_timeout'] = 60 ``` - + ```ruby # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com gitlab_rails['incoming_email_enabled'] = true @@ -253,7 +280,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. idle_timeout: 60 ``` - + ```yaml # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com incoming_email: diff --git a/doc/api/wikis.md b/doc/api/wikis.md index 10eebc4a3c1..15ce5f96b60 100644 --- a/doc/api/wikis.md +++ b/doc/api/wikis.md @@ -155,3 +155,5 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi ``` On success the HTTP status code is `204` and no JSON response is expected. + +[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372 diff --git a/doc/ci/environments.md b/doc/ci/environments.md index c1362b7bd5b..acd5682841a 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -240,55 +240,18 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! -## Web terminals - ->**Note:** -Web terminals were added in GitLab 8.15 and are only available to project -masters and owners. - -If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes service][kubernetes-service], GitLab can open -a terminal session to your environment! This is a very powerful feature that -allows you to debug issues without leaving the comfort of your web browser. To -enable it, just follow the instructions given in the service documentation. - -Once enabled, your environments will gain a "terminal" button: - -![Terminal button on environment index](img/environments_terminal_button_on_index.png) - -You can also access the terminal button from the page for a specific environment: - -![Terminal button for an environment](img/environments_terminal_button_on_show.png) - -Wherever you find it, clicking the button will take you to a separate page to -establish the terminal session: - -![Terminal page](img/environments_terminal_page.png) - -This works just like any other terminal - you'll be in the container created -by your deployment, so you can run shell commands and get responses in real -time, check the logs, try out configuration or code tweaks, etc. You can open -multiple terminals to the same environment - they each get their own shell -session - and even a multiplexer like `screen` or `tmux`! - ->**Note:** -Container-based deployments often lack basic tools (like an editor), and may -be stopped or restarted at any time. If this happens, you will lose all your -changes! Treat this as a debugging tool, not a comprehensive online IDE. - ---- - -While this is fine for deploying to some stable environments like staging or -production, what happens for branches? So far we haven't defined anything -regarding deployments for branches other than `master`. Dynamic environments -will help us achieve that. - ## Dynamic environments As the name suggests, it is possible to create environments on the fly by just declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is the basis of [Review apps](review_apps/index.md). +>**Note:** +The `name` and `url` parameters can use any of the defined CI variables, +including predefined, secure variables and `.gitlab-ci.yml` +[`variables`](yaml/README.md#variables). +You however cannot use variables defined under `script` or on the Runner's side. + GitLab Runner exposes various [environment variables][variables] when a job runs, and as such, you can use them as environment names. Let's add another job in our example which will deploy to all branches except `master`: @@ -434,7 +397,8 @@ Let's briefly see where URL that's defined in the environments is exposed. ## Making use of the environment URL -The environment URL is exposed in a few places within GitLab. +The [environment URL](yaml/README.md#environments-url) is exposed in a few +places within GitLab. | In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button | | -------------------- | ------------ | ----------- | @@ -598,7 +562,7 @@ exist, you should see something like: >**Notes:** > -- For the monitor dashboard to appear, you need to: +- For the monitoring dashboard to appear, you need to: - Have enabled the [Prometheus integration][prom] - Configured Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/metrics.md) - With GitLab 9.2, all deployments to an environment are shown directly on the @@ -608,8 +572,7 @@ If you have enabled [Prometheus for monitoring system and response metrics](http Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any environment which has had a successful deployment. If monitoring data was -successfully retrieved, a Monitoring button will appear on the environment's -detail page. +successfully retrieved, a Monitoring button will appear for each environment. ![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) @@ -623,6 +586,49 @@ version of the app, all without leaving GitLab. ![Monitoring dashboard](img/environments_monitoring.png) +## Web terminals + +>**Note:** +Web terminals were added in GitLab 8.15 and are only available to project +masters and owners. + +If you deploy to your environments with the help of a deployment service (e.g., +the [Kubernetes service][kubernetes-service], GitLab can open +a terminal session to your environment! This is a very powerful feature that +allows you to debug issues without leaving the comfort of your web browser. To +enable it, just follow the instructions given in the service documentation. + +Once enabled, your environments will gain a "terminal" button: + +![Terminal button on environment index](img/environments_terminal_button_on_index.png) + +You can also access the terminal button from the page for a specific environment: + +![Terminal button for an environment](img/environments_terminal_button_on_show.png) + +Wherever you find it, clicking the button will take you to a separate page to +establish the terminal session: + +![Terminal page](img/environments_terminal_page.png) + +This works just like any other terminal - you'll be in the container created +by your deployment, so you can run shell commands and get responses in real +time, check the logs, try out configuration or code tweaks, etc. You can open +multiple terminals to the same environment - they each get their own shell +session - and even a multiplexer like `screen` or `tmux`! + +>**Note:** +Container-based deployments often lack basic tools (like an editor), and may +be stopped or restarted at any time. If this happens, you will lose all your +changes! Treat this as a debugging tool, not a comprehensive online IDE. + +--- + +While this is fine for deploying to some stable environments like staging or +production, what happens for branches? So far we haven't defined anything +regarding deployments for branches other than `master`. Dynamic environments +will help us achieve that. + ## Checkout deployments locally Since 8.13, a reference in the git repository is saved for each deployment, so diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png Binary files differindex d9c46ea4c95..dcffdd1fdb8 100644 --- a/doc/ci/img/environments_monitoring.png +++ b/doc/ci/img/environments_monitoring.png diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 88e53ff40e8..2d56b2540ef 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -106,7 +106,7 @@ What is important is that each job is run independently from each other. If you want to check whether your `.gitlab-ci.yml` file is valid, there is a Lint tool under the page `/ci/lint` of your GitLab instance. You can also find -a "CI Lint" button to go to this page under **Pipelines ➔ Pipelines** and +a "CI Lint" button to go to this page under **CI/CD ➔ Pipelines** and **Pipelines ➔ Jobs** in your project. For more information and a complete `.gitlab-ci.yml` syntax, please read @@ -155,7 +155,7 @@ Find more information about different Runners in the [Runners](../runners/README.md) documentation. You can find whether any Runners are assigned to your project by going to -**Settings ➔ Pipelines**. Setting up a Runner is easy and straightforward. The +**Settings ➔ CI/CD**. Setting up a Runner is easy and straightforward. The official Runner supported by GitLab is written in Go and its documentation can be found at <https://docs.gitlab.com/runner/>. @@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as described in the next section. Once the Runner has been set up, you should see it on the Runners page of your -project, following **Settings ➔ Pipelines**. +project, following **Settings ➔ CI/CD**. ![Activated runners](img/runners_activated.png) @@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can build any project. To enable the **Shared Runners** you have to go to your project's -**Settings ➔ Pipelines** and click **Enable shared runners**. +**Settings ➔ CI/CD** and click **Enable shared runners**. [Read more on Shared Runners](../runners/README.md). diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index bac8e972754..8b51d112a2c 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -35,7 +35,7 @@ are: A Runner that is specific only runs for the specified project(s). A shared Runner can run jobs for every project that has enabled the option **Allow shared Runners** -under **Settings ➔ Pipelines**. +under **Settings ➔ CI/CD**. Projects with high demand of CI activity can also benefit from using specific Runners. By having dedicated Runners you are guaranteed that the Runner is not @@ -61,7 +61,7 @@ You can only register a shared Runner if you are an admin of the GitLab instance Shared Runners are enabled by default as of GitLab 8.2, but can be disabled with the **Disable shared Runners** button which is present under each project's -**Settings ➔ Pipelines** page. Previous versions of GitLab defaulted shared +**Settings ➔ CI/CD** page. Previous versions of GitLab defaulted shared Runners to disabled. ## Registering a specific Runner @@ -76,7 +76,7 @@ Registering a specific can be done in two ways: To create a specific Runner without having admin rights to the GitLab instance, visit the project you want to make the Runner work for in GitLab: -1. Go to **Settings ➔ Pipelines** to obtain the token +1. Go to **Settings ➔ CI/CD** to obtain the token 1. [Register the Runner][register] ### Making an existing shared Runner specific @@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings. To lock/unlock a Runner: -1. Visit your project's **Settings ➔ Pipelines** +1. Visit your project's **Settings ➔ CI/CD** 1. Find the Runner you wish to lock/unlock and make sure it's enabled 1. Click the pencil button 1. Check the **Lock to current projects** option @@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis To enable/disable a Runner in your project: -1. Visit your project's **Settings ➔ Pipelines** +1. Visit your project's **Settings ➔ CI/CD** 1. Find the Runner you wish to enable/disable 1. Click **Enable for this project** or **Disable for this project** @@ -136,7 +136,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on To protect/unprotect Runners: -1. Visit your project's **Settings ➔ Pipelines** +1. Visit your project's **Settings ➔ CI/CD** 1. Find a Runner you want to protect/unprotect and make sure it's enabled 1. Click the pencil button besides the Runner name 1. Check the **Protected** option @@ -220,7 +220,7 @@ each Runner's settings. To make a Runner pick tagged/untagged jobs: -1. Visit your project's **Settings ➔ Pipelines** +1. Visit your project's **Settings ➔ CI/CD** 1. Find the Runner you wish and make sure it's enabled 1. Click the pencil button 1. Check the **Run untagged jobs** option diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index cdb9858e179..e5a2bbd1773 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -34,7 +34,7 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a passphrase to the SSH key, or the `before_script` will prompt for it. Then, create a new **Secret Variable** in your project settings on GitLab -following **Settings > Pipelines** and look for the "Secret Variables" section. +following **Settings > CI/CD** and look for the "Secret Variables" section. As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 7ec7136d8c6..56a16f77e7f 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -19,7 +19,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new ## Adding a new trigger You can add a new trigger by going to your project's -**Settings ➔ Pipelines** under **Triggers**. The **Add trigger** button will +**Settings ➔ CI/CD** under **Triggers**. The **Add trigger** button will create a new token which you can then use to trigger a rerun of this particular project's pipeline. @@ -43,7 +43,7 @@ From now on the trigger will be run as you. ## Revoking a trigger You can revoke a trigger any time by going at your project's -**Settings ➔ Pipelines** under **Triggers** and hitting the **Revoke** button. +**Settings ➔ CI/CD** under **Triggers** and hitting the **Revoke** button. The action is irreversible. ## Triggering a pipeline @@ -64,7 +64,7 @@ POST /projects/:id/trigger/pipeline The required parameters are the [trigger's `token`](#authentication-tokens) and the Git `ref` on which the trigger will be performed. Valid refs are the branch and the tag. The `:id` of a project can be found by -[querying the API](../../api/projects.md) or by visiting the **Pipelines** +[querying the API](../../api/projects.md) or by visiting the **CI/CD** settings page which provides self-explanatory examples. When a rerun of a pipeline is triggered, the information is exposed in GitLab's diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 6513b31826a..ebcb92b5db1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -158,17 +158,17 @@ script: settings. Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. -GitLab CI allows you to define per-project or per-group **secret variables** -that are set in the build environment. The secret variables are stored out of -the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner -making them available in the build environment. It's the recommended method to -use for storing things like passwords, secret keys and credentials. +GitLab CI allows you to define per-project or per-group secret variables +that are set in the pipeline environment. The secret variables are stored out of +the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner +making them available during a pipeline run. It's the recommended method to +use for storing things like passwords, SSH keys and credentials. Project-level secret variables can be added by going to your project's -**Settings ➔ Pipelines**, then finding the section called **Secret variables**. +**Settings > CI/CD**, then finding the section called **Secret variables**. Likewise, group-level secret variables can be added by going to your group's -**Settings ➔ Pipelines**, then finding the section called **Secret variables**. +**Settings > CI/CD**, then finding the section called **Secret variables**. Any variables of [subgroups] will be inherited recursively. Once you set them, they will be available for all subsequent pipelines. You can also @@ -185,8 +185,8 @@ protected, it would only be securely passed to pipelines running on the protected variables. Protected variables can be added by going to your project's -**Settings ➔ Pipelines**, then finding the section called -**Secret variables**, and check *Protected*. +**Settings > CI/CD**, then finding the section called +**Secret variables**, and check "Protected". Once you set them, they will be available for all subsequent pipelines. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 78733b9cc4b..f69d71a5c39 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -727,6 +727,9 @@ deployment to the `production` environment. - Before GitLab 8.11, the name of an environment could be defined as a string like `environment: production`. The recommended way now is to define it under the `name` keyword. +- The `name` parameter can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. The `environment` name can contain: @@ -762,6 +765,9 @@ deploy to production: - Introduced in GitLab 8.11. - Before GitLab 8.11, the URL could be added only in GitLab's UI. The recommended way now is to define it in `.gitlab-ci.yml`. +- The `url` parameter can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. This is an optional value that when set, it exposes buttons in various places in GitLab which when clicked take you to the defined URL. @@ -841,10 +847,9 @@ The `stop_review_app` job is **required** to have the following keywords defined **Notes:** - [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. - The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15. - -`environment` can also represent a configuration hash with `name` and `url`. -These parameters can use any of the defined [CI variables](#variables) -(including predefined, secure variables and `.gitlab-ci.yml` variables). +- The `name` and `url` parameters can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. For example: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 0c17905aa8c..44ee994a26b 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -253,7 +253,7 @@ only. [^1]: On public and internal projects, all users are able to perform this action. [^2]: Guest users can only view the confidential issues they created themselves -[^3]: If **Public pipelines** is enabled in **Project Settings > Pipelines** +[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD** [^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner [^5]: Only if user is not external one. [^6]: Only if user is a member of the project. diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 2a37cbd160b..17a47cfa646 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -11,7 +11,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | | HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | -## Configuring Prometheus to monitor for NGINX ingress metrics +## Configuring NGINX ingress monitoring If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, and your application is running in the same cluster, no further action is required. The ingress metrics will be automatically enabled and annotated for Prometheus monitoring. Simply ensure Prometheus monitoring is [enabled for your project](../prometheus.md), which is on by default. @@ -20,14 +20,14 @@ For other deployments, there is some configuration required depending on your in * NGINX Ingress should be annotated for Prometheus monitoring * Prometheus should be configured to monitor annotated pods -### Configuring NGINX Ingress for Prometheus monitoring +### Setting up NGINX Ingress for Prometheus monitoring Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254. -With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. NGINX ingress metrics are exposed per pod, a sample scrape configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring. +With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. Since NGINX ingress metrics are exposed per pod, a scrape job for Kubernetes pods is required. A sample pod scraping configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring. Depending on how NGINX ingress was deployed, typically a DaemonSet or Deployment, edit the corresponding YML spec. Two new annotations need to be added: -* `prometheus.io/port: "true"` +* `prometheus.io/scrape: "true"` * `prometheus.io/port: "10254"` Prometheus should now be collecting NGINX ingress metrics. To validate view the Prometheus Targets, available under `Status > Targets` on the Prometheus dashboard. New entries for NGINX should be listed in the kubernetes pod monitoring job, `kubernetes-pods`. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index dbc1305101f..56f58fd755a 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,7 +1,7 @@ # Pipelines settings To reach the pipelines settings navigate to your project's -**Settings ➔ Pipelines**. +**Settings ➔ CI/CD**. The following settings can be configured per project. diff --git a/doc/user/project/repository/img/compare_branches.png b/doc/user/project/repository/img/compare_branches.png Binary files differindex 353bd72ef4e..d7ab587f030 100755..100644 --- a/doc/user/project/repository/img/compare_branches.png +++ b/doc/user/project/repository/img/compare_branches.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index bcc3625f908..2b23c494dc4 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -31,8 +31,8 @@ on the search field on the top-right of your screen: If you want to search for issues present in a specific project, navigate to a project's **Issues** tab, and click on the field **Search or filter results...**. It will -display a dropdown menu, from which you can add filters per author, assignee, milestone, label, -and weight. When done, press **Enter** on your keyboard to filter the issues. +display a dropdown menu, from which you can add filters per author, assignee, milestone, +label, weight, and 'my-reaction' (based on your emoji votes). When done, press **Enter** on your keyboard to filter the issues. ![filter issues in a project](img/issue_search_filter.png) diff --git a/features/project/archived.feature b/features/project/archived.feature deleted file mode 100644 index ad466f4f307..00000000000 --- a/features/project/archived.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Project Archived - Background: - Given I sign in as a user - And I own project "Shop" - And I own project "Forum" - - Scenario: I should not see archived on project page of not-archive project - And project "Forum" is archived - And I visit project "Shop" page - Then I should not see "Archived" - - Scenario: I should see archived on project page of archive project - And project "Forum" is archived - And I visit project "Forum" page - Then I should see "Archived" - - Scenario: I archive project - When project "Shop" has push event - And I visit project "Shop" page - And I visit edit project "Shop" page - And I set project archived - Then I should see "Archived" - - Scenario: I unarchive project - When project "Shop" has push event - And project "Shop" is archived - And I visit project "Shop" page - And I visit edit project "Shop" page - And I set project unarchived - Then I should not see "Archived" diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature deleted file mode 100644 index 7ee1d717d80..00000000000 --- a/features/project/commits/revert.feature +++ /dev/null @@ -1,31 +0,0 @@ -@project_commits -Feature: Revert Commits - Background: - Given I sign in as a user - And I own a project - And I visit my project's commits page - - @javascript - Scenario: I revert a commit - Given I click on commit link - And I click on the revert button - And I revert the changes directly - Then I should see the revert commit notice - - @javascript - Scenario: I revert a commit that was previously reverted - Given I click on commit link - And I click on the revert button - And I revert the changes directly - And I visit my project's commits page - And I click on commit link - And I click on the revert button - And I revert the changes directly - Then I should see a revert error - - @javascript - Scenario: I revert a commit in a new merge request - Given I click on commit link - And I click on the revert button - And I revert the changes in a new merge request - Then I should see the new merge request notice diff --git a/features/search.feature b/features/search.feature deleted file mode 100644 index f894b6b84a1..00000000000 --- a/features/search.feature +++ /dev/null @@ -1,100 +0,0 @@ -@dashboard -Feature: Search - Background: - Given I sign in as a user - And I own project "Shop" - And I visit dashboard search page - - Scenario: I should see project I am looking for - Given I search for "Sho" - Then I should see "Shop" project link - - @javascript - Scenario: I should see issues I am looking for - And project has issues - When I search for "Foo" - And I click "Issues" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see merge requests I am looking for - And project has merge requests - When I search for "Foo" - When I click "Merge requests" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see milestones I am looking for - And project has milestones - When I search for "Foo" - When I click "Milestones" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see project code I am looking for - When I click project "Shop" link - And I search for "rspec" - Then I should see code results for project "Shop" - - @javascript - Scenario: I should see project issues - And project has issues - When I click project "Shop" link - And I search for "Foo" - And I click "Issues" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see project merge requests - And project has merge requests - When I click project "Shop" link - And I search for "Foo" - And I click "Merge requests" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see project milestones - And project has milestones - When I click project "Shop" link - And I search for "Foo" - And I click "Milestones" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - @javascript - Scenario: I should see Wiki blobs - And project has Wiki content - When I click project "Shop" link - And I search for "Wiki content" - And I click "Wiki" link - Then I should see "test_wiki" link in the search results - - Scenario: I logout and should see project I am looking for - Given project "Shop" is public - And I logout directly - And I visit dashboard search page - And I search for "Sho" - Then I should see "Shop" project link - - @javascript - Scenario: I logout and should see issues I am looking for - Given project "Shop" is public - And I logout directly - And I visit dashboard search page - And project has issues - When I search for "Foo" - And I click "Issues" link - Then I should see "Foo" link in the search results - And I should not see "Bar" link in the search results - - Scenario: I logout and should see project code I am looking for - Given project "Shop" is public - And I logout directly - When I visit project "Shop" page - And I search for "rspec" on project page - Then I should see code results for project "Shop" diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb deleted file mode 100644 index e4847180be9..00000000000 --- a/features/steps/project/archived.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Spinach::Features::ProjectArchived < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - When 'project "Forum" is archived' do - project = Project.find_by(name: "Forum") - project.update_attribute(:archived, true) - end - - When 'project "Shop" is archived' do - project = Project.find_by(name: "Shop") - project.update_attribute(:archived, true) - end - - When 'I visit project "Forum" page' do - project = Project.find_by(name: "Forum") - visit project_path(project) - end - - step 'I should not see "Archived"' do - expect(page).not_to have_content "Archived" - end - - step 'I should see "Archived"' do - expect(page).to have_content "Archived" - end - - When 'I set project archived' do - click_link "Archive" - end - - When 'I set project unarchived' do - click_link "Unarchive" - end -end diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb deleted file mode 100644 index ebfa7a878bb..00000000000 --- a/features/steps/project/commits/revert.rb +++ /dev/null @@ -1,42 +0,0 @@ -class Spinach::Features::RevertCommits < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include SharedDiffNote - include RepoHelpers - - step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) - end - - step 'I click on the revert button' do - find(".header-action-buttons .dropdown").click - find("a[href='#modal-revert-commit']").click - end - - step 'I revert the changes directly' do - page.within('#modal-revert-commit') do - uncheck 'create_merge_request' - click_button 'Revert' - end - end - - step 'I should see the revert commit notice' do - page.should have_content('The commit has been successfully reverted.') - end - - step 'I should see a revert error' do - page.should have_content('Sorry, we cannot revert this commit automatically.') - end - - step 'I revert the changes in a new merge request' do - page.within('#modal-revert-commit') do - click_button 'Revert' - end - end - - step 'I should see the new merge request notice' do - page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') - page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") - end -end diff --git a/features/steps/search.rb b/features/steps/search.rb deleted file mode 100644 index 16c4a5ab2e4..00000000000 --- a/features/steps/search.rb +++ /dev/null @@ -1,116 +0,0 @@ -class Spinach::Features::Search < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I search for "Sho"' do - fill_in "dashboard_search", with: "Sho" - click_button "Search" - end - - step 'I search for "Foo"' do - fill_in "dashboard_search", with: "Foo" - find('.btn-search').trigger('click') - end - - step 'I search for "rspec"' do - fill_in "dashboard_search", with: "rspec" - find('.btn-search').trigger('click') - end - - step 'I search for "rspec" on project page' do - fill_in "search", with: "rspec" - click_button "Go" - end - - step 'I search for "Wiki content"' do - fill_in "dashboard_search", with: "content" - find('.btn-search').trigger('click') - end - - step 'I click "Issues" link' do - page.within '.search-filter' do - click_link 'Issues' - end - end - - step 'I click project "Shop" link' do - find('.js-search-project-dropdown').trigger('click') - page.within '.project-filter' do - click_link project.name_with_namespace - end - end - - step 'I click "Merge requests" link' do - page.within '.search-filter' do - click_link 'Merge requests' - end - end - - step 'I click "Milestones" link' do - page.within '.search-filter' do - click_link 'Milestones' - end - end - - step 'I click "Wiki" link' do - page.within '.search-filter' do - click_link 'Wiki' - end - end - - step 'I should see "Shop" project link' do - expect(page).to have_link "Shop" - end - - step 'I should see code results for project "Shop"' do - page.within('.results') do - page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions' - end - end - - step 'I search for "Contibuting"' do - fill_in "dashboard_search", with: "Contibuting" - click_button "Search" - end - - step 'project has issues' do - create(:issue, title: "Foo", project: project) - create(:issue, title: "Bar", project: project) - end - - step 'project has merge requests' do - create(:merge_request, title: "Foo", source_project: project, target_project: project) - create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project) - end - - step 'project has milestones' do - create(:milestone, title: "Foo", project: project) - create(:milestone, title: "Bar", project: project) - end - - step 'I should see "Foo" link in the search results' do - page.within('.results') do - find(:css, '.search-results').should have_link 'Foo' - end - end - - step 'I should not see "Bar" link in the search results' do - expect(find(:css, '.search-results')).not_to have_link 'Bar' - end - - step 'I should see "test_wiki" link in the search results' do - page.within('.results') do - expect(find(:css, '.search-results')).to have_link 'test_wiki' - end - end - - step 'project has Wiki content' do - @wiki = ::ProjectWiki.new(project, current_user) - @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit") - end - - step 'project "Shop" is public' do - project.update_attributes(visibility_level: Project::PUBLIC) - end -end diff --git a/lib/api/users.rb b/lib/api/users.rb index 1825c90a23b..bdebda58d3f 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -88,7 +88,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) - opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {} + opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User } present user, opts end diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb new file mode 100644 index 00000000000..b1411be3016 --- /dev/null +++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb @@ -0,0 +1,41 @@ +module Gitlab + module BackgroundMigration + class DeleteConflictingRedirectRoutesRange + class Route < ActiveRecord::Base + self.table_name = 'routes' + end + + class RedirectRoute < ActiveRecord::Base + self.table_name = 'redirect_routes' + end + + # start_id - The start ID of the range of events to process + # end_id - The end ID of the range to process. + def perform(start_id, end_id) + return unless migrate? + + conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id)) + num_rows = conflicts.delete_all + + Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.") + end + + def migrate? + Route.table_exists? && RedirectRoute.table_exists? + end + + def routes_match_redirects_clause(start_id, end_id) + <<~ROUTES_MATCH_REDIRECTS + EXISTS ( + SELECT 1 FROM routes + WHERE ( + LOWER(redirect_routes.path) = LOWER(routes.path) + OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%')) + ) + AND routes.id BETWEEN #{start_id} AND #{end_id} + ) + ROUTES_MATCH_REDIRECTS + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index fb14798efe6..2c35da8f1aa 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1,6 +1,9 @@ module Gitlab module Database module MigrationHelpers + BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job + BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time + # Adds `created_at` and `updated_at` columns with timezone information. # # This method is an improved version of Rails' built-in method `add_timestamps`. @@ -653,6 +656,91 @@ into similar problems in the future (e.g. when new tables are created). EOF end end + + # Bulk queues background migration jobs for an entire table, batched by ID range. + # "Bulk" meaning many jobs will be pushed at a time for efficiency. + # If you need a delay interval per job, then use `queue_background_migration_jobs_by_range_at_intervals`. + # + # model_class - The table being iterated over + # job_class_name - The background migration job class as a string + # batch_size - The maximum number of rows per job + # + # Example: + # + # class Route < ActiveRecord::Base + # include EachBatch + # self.table_name = 'routes' + # end + # + # bulk_queue_background_migration_jobs_by_range(Route, 'ProcessRoutes') + # + # Where the model_class includes EachBatch, and the background migration exists: + # + # class Gitlab::BackgroundMigration::ProcessRoutes + # def perform(start_id, end_id) + # # do something + # end + # end + def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE) + raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id') + + jobs = [] + + model_class.each_batch(of: batch_size) do |relation| + start_id, end_id = relation.pluck('MIN(id), MAX(id)').first + + if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE + # Note: This code path generally only helps with many millions of rows + # We push multiple jobs at a time to reduce the time spent in + # Sidekiq/Redis operations. We're using this buffer based approach so we + # don't need to run additional queries for every range. + BackgroundMigrationWorker.perform_bulk(jobs) + jobs.clear + end + + jobs << [job_class_name, [start_id, end_id]] + end + + BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty? + end + + # Queues background migration jobs for an entire table, batched by ID range. + # Each job is scheduled with a `delay_interval` in between. + # If you use a small interval, then some jobs may run at the same time. + # + # model_class - The table being iterated over + # job_class_name - The background migration job class as a string + # delay_interval - The duration between each job's scheduled time (must respond to `to_f`) + # batch_size - The maximum number of rows per job + # + # Example: + # + # class Route < ActiveRecord::Base + # include EachBatch + # self.table_name = 'routes' + # end + # + # queue_background_migration_jobs_by_range_at_intervals(Route, 'ProcessRoutes', 1.minute) + # + # Where the model_class includes EachBatch, and the background migration exists: + # + # class Gitlab::BackgroundMigration::ProcessRoutes + # def perform(start_id, end_id) + # # do something + # end + # end + def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE) + raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id') + + model_class.each_batch(of: batch_size) do |relation, index| + start_id, end_id = relation.pluck('MIN(id), MAX(id)').first + + # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for + # the same time, which is not helpful in most cases where we wish to + # spread the work over time. + BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id]) + end + end end end end diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb new file mode 100644 index 00000000000..4571ad122ce --- /dev/null +++ b/lib/gitlab/database/read_only_relation.rb @@ -0,0 +1,16 @@ +module Gitlab + module Database + # Module that can be injected into a ActiveRecord::Relation to make it + # read-only. + module ReadOnlyRelation + [:delete, :delete_all, :update, :update_all].each do |method| + define_method(method) do |*args| + raise( + ActiveRecord::ReadOnlyRecord, + "This relation is marked as read-only" + ) + end + end + end + end +end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index 9e6fca8c80c..dcdec818f5e 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -1,11 +1,18 @@ module Gitlab module Git class OperationService - attr_reader :committer, :repository + WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do + alias_method :repo_created?, :repo_created + alias_method :branch_created?, :branch_created + end + + attr_reader :user, :repository - def initialize(committer, new_repository) - committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User) - @committer = committer + def initialize(user, new_repository) + if user + user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) + @user = user + end # Refactoring aid unless new_repository.is_a?(Gitlab::Git::Repository) @@ -105,7 +112,7 @@ module Gitlab ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name update_ref_in_hooks(ref, newrev, oldrev) - [newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)] + WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)) end def find_oldrev_from_branch(newrev, branch) @@ -128,7 +135,7 @@ module Gitlab def with_hooks(ref, newrev, oldrev) Gitlab::Git::HooksService.new.execute( - committer, + user, repository, oldrev, newrev, diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index efa13590a2c..c499ff101b5 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -610,49 +610,86 @@ module Gitlab # TODO: implement this method end - def add_branch(branch_name, committer:, target:) + def add_branch(branch_name, user:, target:) target_object = Ref.dereference_object(lookup(target)) raise InvalidRef.new("target not found: #{target}") unless target_object - OperationService.new(committer, self).add_branch(branch_name, target_object.oid) + OperationService.new(user, self).add_branch(branch_name, target_object.oid) find_branch(branch_name) rescue Rugged::ReferenceError => ex raise InvalidRef, ex end - def add_tag(tag_name, committer:, target:, message: nil) + def add_tag(tag_name, user:, target:, message: nil) target_object = Ref.dereference_object(lookup(target)) raise InvalidRef.new("target not found: #{target}") unless target_object - committer = Committer.from_user(committer) if committer.is_a?(User) + user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) options = nil # Use nil, not the empty hash. Rugged cares about this. if message options = { message: message, - tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name) + tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name) } end - OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options) + OperationService.new(user, self).add_tag(tag_name, target_object.oid, options) find_tag(tag_name) rescue Rugged::ReferenceError => ex raise InvalidRef, ex end - def rm_branch(branch_name, committer:) - OperationService.new(committer, self).rm_branch(find_branch(branch_name)) + def rm_branch(branch_name, user:) + OperationService.new(user, self).rm_branch(find_branch(branch_name)) end - def rm_tag(tag_name, committer:) - OperationService.new(committer, self).rm_tag(find_tag(tag_name)) + def rm_tag(tag_name, user:) + OperationService.new(user, self).rm_tag(find_tag(tag_name)) end def find_tag(name) tags.find { |tag| tag.name == name } end + def merge(user, source_sha, target_branch, message) + committer = Gitlab::Git.committer_hash(email: user.email, name: user.name) + + OperationService.new(user, self).with_branch(target_branch) do |start_commit| + our_commit = start_commit.sha + their_commit = source_sha + + raise 'Invalid merge target' unless our_commit + raise 'Invalid merge source' unless their_commit + + merge_index = rugged.merge_commits(our_commit, their_commit) + break if merge_index.conflicts? + + options = { + parents: [our_commit, their_commit], + tree: merge_index.write_tree(rugged), + message: message, + author: committer, + committer: committer + } + + commit_id = create_commit(options) + + yield commit_id + + commit_id + end + rescue Gitlab::Git::CommitError # when merge_index.conflicts? + nil + end + + def create_commit(params = {}) + params[:message].delete!("\r") + + Rugged::Commit.create(rugged, params) + end + # Delete the specified branch from the repository def delete_branch(branch_name) gitaly_migrate(:delete_branch) do |is_enabled| diff --git a/lib/gitlab/git/committer.rb b/lib/gitlab/git/user.rb index 1f4bcf7a3a0..ea634d39668 100644 --- a/lib/gitlab/git/committer.rb +++ b/lib/gitlab/git/user.rb @@ -1,10 +1,14 @@ module Gitlab module Git - class Committer + class User attr_reader :name, :email, :gl_id - def self.from_user(user) - new(user.name, user.email, Gitlab::GlId.gl_id(user)) + def self.from_gitlab(gitlab_user) + new(gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user)) + end + + def self.from_gitaly(gitaly_user) + new(gitaly_user.name, gitaly_user.email, gitaly_user.gl_id) end def initialize(name, email, gl_id) diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb index 5a31e56cb30..635f52131f9 100644 --- a/lib/gitlab/group_hierarchy.rb +++ b/lib/gitlab/group_hierarchy.rb @@ -22,7 +22,7 @@ module Gitlab def base_and_ancestors return ancestors_base unless Group.supports_nested_groups? - base_and_ancestors_cte.apply_to(model.all) + read_only(base_and_ancestors_cte.apply_to(model.all)) end # Returns a relation that includes the descendants_base set of groups @@ -30,7 +30,7 @@ module Gitlab def base_and_descendants return descendants_base unless Group.supports_nested_groups? - base_and_descendants_cte.apply_to(model.all) + read_only(base_and_descendants_cte.apply_to(model.all)) end # Returns a relation that includes the base groups, their ancestors, @@ -67,11 +67,13 @@ module Gitlab union = SQL::Union.new([model.unscoped.from(ancestors_table), model.unscoped.from(descendants_table)]) - model + relation = model .unscoped .with .recursive(ancestors.to_arel, descendants.to_arel) .from("(#{union.to_sql}) #{model.table_name}") + + read_only(relation) end private @@ -107,5 +109,12 @@ module Gitlab def groups_table model.arel_table end + + def read_only(relation) + # relations using a CTE are not safe to use with update_all as it will + # throw away the CTE, hence we mark them as read-only. + relation.extend(Gitlab::Database::ReadOnlyRelation) + relation + end end end diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 1f331b1e91d..5b5ed449f94 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -13,7 +13,7 @@ module Gitlab end def provider - @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s) + @provider ||= auth_hash.provider.to_s end def name diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 2577d98df6f..7ce6a61d50c 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Group merge requests page' do end it 'ignores archived merge request count badges in navbar' do - expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1") + expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1") end it 'ignores archived merge request count badges in state-filters' do diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb new file mode 100644 index 00000000000..5ed4f3ad2bc --- /dev/null +++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'Groups > User sees users dropdowns in issuables list' do + let(:entity) { create(:group) } + let(:user_in_dropdown) { create(:user) } + let!(:user_not_in_dropdown) { create(:user) } + let!(:project) { create(:project, group: entity) } + + before do + entity.add_developer(user_in_dropdown) + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:issue, project: project) } + let(:issuables_path) { issues_group_path(entity) } + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:merge_request, source_project: project) } + let(:issuables_path) { merge_requests_group_path(entity) } + end +end diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 624f13922ed..50c5e0bb65f 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -18,9 +18,9 @@ describe 'Milestone show' do it 'avoids N+1 database queries' do create(:labeled_issue, issue_params) - control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + control = ActiveRecord::QueryRecorder.new { visit_milestone } create_list(:labeled_issue, 10, issue_params) - expect { visit_milestone }.not_to exceed_query_limit(control_count) + expect { visit_milestone }.not_to exceed_query_limit(control) end end diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb index 8c7233c77ad..a8c08a680d7 100644 --- a/spec/features/profiles/user_visits_profile_account_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb @@ -10,7 +10,6 @@ describe 'User visits the profile account page' do end it 'shows correct menu item' do - expect(find('.sidebar-top-level-items > li.active')).to have_content('Account') - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + expect(page).to have_active_navigation('Account') end end diff --git a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb deleted file mode 100644 index ffb504cc573..00000000000 --- a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'User visits the authentication log page' do - let(:user) { create(:user) } - - before do - sign_in(user) - - visit(audit_log_profile_path) - end - - it 'shows correct menu item' do - expect(find('.sidebar-top-level-items > li.active')).to have_content('Authentication log') - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) - end -end diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb new file mode 100644 index 00000000000..a50ebb29e01 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits the authentication log' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(audit_log_profile_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Authentication log') + end +end diff --git a/spec/features/profiles/user_visits_profile_page_spec.rb b/spec/features/profiles/user_visits_profile_page_spec.rb deleted file mode 100644 index 3bf6d718bc7..00000000000 --- a/spec/features/profiles/user_visits_profile_page_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'User visits the profile page' do - let(:user) { create(:user) } - - before do - sign_in(user) - - visit(profile_path) - end - - it 'shows correct menu item' do - expect(find('.sidebar-top-level-items > li.active')).to have_content('Profile') - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) - end -end diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index d1776b3d7c2..924ee0e4174 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -10,8 +10,7 @@ describe 'User visits the profile preferences page' do end it 'shows correct menu item' do - expect(find('.sidebar-top-level-items > li.active')).to have_content('Preferences') - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + expect(page).to have_active_navigation('Preferences') end describe 'User changes their syntax highlighting theme', :js do diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb new file mode 100644 index 00000000000..6601d3039ed --- /dev/null +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'User visits their profile' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_path) + end + + it 'shows correct menu item' do + expect(page).to have_active_navigation('Profile') + end +end diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb index 0b7a63b54b4..685bf44619d 100644 --- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb @@ -10,7 +10,6 @@ describe 'User visits the profile SSH keys page' do end it 'shows correct menu item' do - expect(find('.sidebar-top-level-items > li.active')).to have_content('SSH Keys') - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + expect(page).to have_active_navigation('SSH Keys') end end diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb new file mode 100644 index 00000000000..221f1d7757e --- /dev/null +++ b/spec/features/projects/commit/user_reverts_commit_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'User reverts a commit', :js do + include RepoHelpers + + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(project_commit_path(project, sample_commit.id)) + + find('.header-action-buttons .dropdown').click + find('a[href="#modal-revert-commit"]').click + end + + context 'without creating a new merge request' do + before do + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + end + + it 'reverts a commit' do + expect(page).to have_content('The commit has been successfully reverted.') + end + + it 'does not revert a previously reverted commit' do + # Visit the comment again once it was reverted. + visit project_commit_path(project, sample_commit.id) + + find('.header-action-buttons .dropdown').click + find('a[href="#modal-revert-commit"]').click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('Sorry, we cannot revert this commit automatically.') + end + end + + context 'with creating a new merge request' do + it 'reverts a commit' do + page.within('#modal-revert-commit') do + click_button('Revert') + end + + expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") + end + end +end diff --git a/spec/features/projects/user_archives_project_spec.rb b/spec/features/projects/user_archives_project_spec.rb new file mode 100644 index 00000000000..72063d13c2a --- /dev/null +++ b/spec/features/projects/user_archives_project_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'User archives a project' do + let(:user) { create(:user) } + + before do + project.add_master(user) + + sign_in(user) + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived, namespace: user.namespace) } + + before do + visit(edit_project_path(project)) + end + + it 'unarchives a project' do + expect(page).to have_content('Unarchive project') + + click_link('Unarchive') + + expect(page).not_to have_content('Archived project') + end + end + + context 'when a project is unarchived' do + let(:project) { create(:project, :repository, namespace: user.namespace) } + + before do + visit(edit_project_path(project)) + end + + it 'archives a project' do + expect(page).to have_content('Archive project') + + click_link('Archive') + + expect(page).to have_content('Archived') + end + end +end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb new file mode 100644 index 00000000000..0ed797a62ea --- /dev/null +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe 'User searches for code' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, namespace: user.namespace) } + + context 'when signed in' do + before do + project.add_master(user) + sign_in(user) + end + + it 'finds a file' do + visit(project_path(project)) + + page.within('.search') do + fill_in('search', with: 'application.js') + click_button('Go') + end + + click_link('Code') + + expect(page).to have_selector('.file-content .code') + expect(page).to have_selector("span.line[lang='javascript']") + end + + context 'when on a project page', :js do + before do + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds code' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: 'rspec') + find('.btn-search').trigger('click') + + page.within('.results') do + expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') + end + end + end + end + + context 'when signed out' do + let(:project) { create(:project, :public, :repository) } + + before do + visit(project_path(project)) + end + + it 'finds code' do + fill_in('search', with: 'rspec') + click_button('Go') + + page.within('.results') do + expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') + end + end + end +end diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb new file mode 100644 index 00000000000..c7c469a262c --- /dev/null +++ b/spec/features/search/user_searches_for_comments_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe 'User searches for comments' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(project_path(project)) + end + + context 'when a comment is in commits' do + context 'when comment belongs to an invalid commit' do + let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') } + + it 'finds a commit' do + page.within('.search') do + fill_in('search', with: comment.note) + click_button('Go') + end + + click_link('Comments') + + expect(page).to have_text('Commit deleted') + expect(page).to have_text('12345678') + end + end + end + + context 'when a comment is in a snippet' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') } + let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) } + + it 'finds a snippet' do + page.within('.search') do + fill_in('search', with: comment.note) + click_button('Go') + end + + click_link('Comments') + + expect(page).to have_link(snippet.title) + end + end +end diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb new file mode 100644 index 00000000000..28cae444588 --- /dev/null +++ b/spec/features/search/user_searches_for_commits_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'User searches for commits' do + let(:project) { create(:project, :repository) } + let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(search_path(project_id: project.id)) + end + + context 'when searching by SHA' do + it 'finds a commit and redirects to its page' do + fill_in('search', with: sha) + click_button('Search') + + expect(page).to have_current_path(project_commit_path(project, sha)) + end + + it 'finds a commit in uppercase and redirects to its page' do + fill_in('search', with: sha.upcase) + click_button('Search') + + expect(page).to have_current_path(project_commit_path(project, sha)) + end + end + + context 'when searching by message' do + it 'finds a commit and holds on /search page' do + create_commit('Message referencing another sha: "deadbeef"', project, user, 'master') + + fill_in('search', with: 'deadbeef') + click_button('Search') + + expect(page).to have_current_path('/search', only_path: true) + end + + it 'finds multiple commits' do + fill_in('search', with: 'See merge request') + click_button('Search') + click_link('Commits') + + expect(page).to have_selector('.commit-row-description', count: 9) + end + end +end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb new file mode 100644 index 00000000000..630a81b1c5e --- /dev/null +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe 'User searches for issues', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:issue1) { create(:issue, title: 'Foo', project: project) } + let!(:issue2) { create(:issue, title: 'Bar', project: project) } + + context 'when signed in' do + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds an issue' do + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + + context 'when on a project page' do + it 'finds an issue' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + end + end + + context 'when signed out' do + let(:project) { create(:project, :public) } + + before do + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds an issue' do + fill_in('dashboard_search', with: issue1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Issues') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb new file mode 100644 index 00000000000..116256682f4 --- /dev/null +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe 'User searches for merge requests', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) } + let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a merge request' do + fill_in('dashboard_search', with: merge_request1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Merge requests') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + end + end + + context 'when on a project page' do + it 'finds a merge request' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: merge_request1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Merge requests') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb new file mode 100644 index 00000000000..4fa9fe9ce8c --- /dev/null +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe 'User searches for milestones', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:milestone1) { create(:milestone, title: 'Foo', project: project) } + let!(:milestone2) { create(:milestone, title: 'Bar', project: project) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a milestone' do + fill_in('dashboard_search', with: milestone1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Milestones') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + end + end + + context 'when on a project page' do + it 'finds a milestone' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: milestone1.title) + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Milestones') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title) + end + end + end +end diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb new file mode 100644 index 00000000000..242e437e41c --- /dev/null +++ b/spec/features/search/user_searches_for_projects_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'User searches for projects' do + let!(:project) { create(:project, :public, name: 'Shop') } + + context 'when signed out' do + include_examples 'top right search form' + + it 'finds a project' do + visit(search_path) + + fill_in('dashboard_search', with: project.name[0..3]) + click_button('Search') + + expect(page).to have_link(project.name) + end + + it 'preserves the group being searched in' do + visit(search_path(group_id: project.namespace.id)) + + fill_in('search', with: 'foo') + click_button('Search') + + expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) + end + + it 'preserves the project being searched in' do + visit(search_path(project_id: project.id)) + + fill_in('search', with: 'foo') + click_button('Search') + + expect(find('#project_id', visible: false).value).to eq(project.id.to_s) + end + end +end diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb new file mode 100644 index 00000000000..1ea56479ecc --- /dev/null +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'User searches for wiki pages', :js do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) } + + before do + project.add_master(user) + sign_in(user) + + visit(search_path) + end + + include_examples 'top right search form' + + it 'finds a page' do + find('.js-search-project-dropdown').trigger('click') + + page.within('.project-filter') do + click_link(project.name_with_namespace) + end + + fill_in('dashboard_search', with: 'content') + find('.btn-search').trigger('click') + + page.within('.search-filter') do + click_link('Wiki') + end + + page.within('.results') do + expect(find(:css, '.search-results')).to have_link(wiki_page.title) + end + end +end diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb new file mode 100644 index 00000000000..5ddea36add5 --- /dev/null +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe 'User uses header search field' do + include FilteredSearchHelpers + + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + + visit(project_path(project)) + end + + it 'starts searching by pressing the enter key', :js do + fill_in('search', with: 'gitlab') + find('#search').native.send_keys(:enter) + + page.within('.breadcrumbs-sub-title') do + expect(page).to have_content('Search') + end + end + + it 'contains location badge' do + expect(page).to have_selector('.has-location-badge') + end + + context 'when clicking the search field', :js do + before do + page.find('#search').click + end + + it 'shows category search dropdown' do + expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) + end + + context 'when clicking issues' do + let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } + + it 'shows assigned issues' do + find('.dropdown-menu').click_link('Issues assigned to me') + + expect(page).to have_selector('.filtered-search') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created issues' do + find('.dropdown-menu').click_link("Issues I've created") + + expect(page).to have_selector('.filtered-search') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + + context 'when clicking merge requests' do + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + + it 'shows assigned merge requests' do + find('.dropdown-menu').click_link('Merge requests assigned to me') + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([assignee_token(user.name)]) + expect_filtered_search_input_empty + end + + it 'shows created merge requests' do + find('.dropdown-menu').click_link("Merge requests I've created") + + expect(page).to have_selector('.merge-requests-holder') + expect_tokens([author_token(user.name)]) + expect_filtered_search_input_empty + end + end + end + + context 'when entering text into the search field', :js do + before do + page.within('.search-input-wrap') do + fill_in('search', with: project.name[0..3]) + end + end + + it 'does not display the category search dropdown' do + expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + end + end +end diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb new file mode 100644 index 00000000000..95f3eb5e805 --- /dev/null +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'User uses search filters', :js do + let(:group) { create(:group) } + let!(:group_project) { create(:project, group: group) } + let(:project) { create(:project, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_reporter(user) + group.add_owner(user) + sign_in(user) + + visit(search_path) + end + + context' when filtering by group' do + it 'shows group projects' do + find('.js-search-group-dropdown').trigger('click') + + wait_for_requests + + page.within('.search-holder') do + click_link(group.name) + end + + expect(find('.js-search-group-dropdown')).to have_content(group.name) + + page.within('.project-filter') do + find('.js-search-project-dropdown').trigger('click') + + wait_for_requests + + expect(page).to have_link(group_project.name_with_namespace) + end + end + end + + context' when filtering by project' do + it 'shows a project' do + page.within('.project-filter') do + find('.js-search-project-dropdown').trigger('click') + + wait_for_requests + + click_link(project.name_with_namespace) + end + + expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) + end + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb deleted file mode 100644 index 8f6d0bb9d1b..00000000000 --- a/spec/features/search_spec.rb +++ /dev/null @@ -1,310 +0,0 @@ -require 'spec_helper' - -describe "Search" do - include FilteredSearchHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - let!(:issue) { create(:issue, project: project, assignees: [user]) } - let!(:issue2) { create(:issue, project: project, author: user) } - - before do - sign_in(user) - project.team << [user, :reporter] - visit search_path - end - - it 'does not show top right search form' do - expect(page).not_to have_selector('.search') - end - - context 'search filters', js: true do - let(:group) { create(:group) } - let!(:group_project) { create(:project, group: group) } - - before do - group.add_owner(user) - end - - it 'shows group name after filtering' do - find('.js-search-group-dropdown').trigger('click') - wait_for_requests - - page.within '.search-holder' do - click_link group.name - end - - expect(find('.js-search-group-dropdown')).to have_content(group.name) - end - - it 'filters by group projects after filtering by group' do - find('.js-search-group-dropdown').trigger('click') - wait_for_requests - - page.within '.search-holder' do - click_link group.name - end - - expect(find('.js-search-group-dropdown')).to have_content(group.name) - - page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') - wait_for_requests - - expect(page).to have_link(group_project.name_with_namespace) - end - end - - it 'shows project name after filtering' do - page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') - wait_for_requests - - click_link project.name_with_namespace - end - - expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) - end - end - - describe 'searching for Projects' do - it 'finds a project' do - page.within '.search-holder' do - fill_in "search", with: project.name[0..3] - click_button "Search" - end - - expect(page).to have_content project.name - end - end - - context 'search for comments' do - context 'when comment belongs to a invalid commit' do - let(:project) { create(:project, :repository) } - let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } - - before do - note.update_attributes(commit_id: 12345678) - end - - it 'finds comment' do - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: note.note - click_button 'Go' - end - - click_link 'Comments' - - expect(page).to have_text("Commit deleted") - expect(page).to have_text("12345678") - end - end - - it 'finds a snippet' do - snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') - note = create(:note, - noteable: snippet, - author: user, - note: 'Supercalifragilisticexpialidocious', - project: project) - # Must visit project dashboard since global search won't search - # everything (e.g. comments, snippets, etc.) - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: note.note - click_button 'Go' - end - - click_link 'Comments' - - expect(page).to have_link(snippet.title) - end - - it 'finds a commit' do - project = create(:project, :repository) { |p| p.add_reporter(user) } - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: 'add' - click_button 'Go' - end - - click_link "Commits" - - expect(page).to have_selector('.commit-row-description') - end - - it 'finds a code' do - project = create(:project, :repository) { |p| p.add_reporter(user) } - visit project_path(project) - - page.within '.search' do - fill_in 'search', with: 'application.js' - click_button 'Go' - end - - click_link "Code" - - expect(page).to have_selector('.file-content .code') - - expect(page).to have_selector("span.line[lang='javascript']") - end - end - - describe 'Right header search field' do - it 'allows enter key to search', js: true do - visit project_path(project) - fill_in 'search', with: 'gitlab' - find('#search').native.send_keys(:enter) - - page.within '.breadcrumbs-sub-title' do - expect(page).to have_content 'Search' - end - end - - describe 'Search in project page' do - before do - visit project_path(project) - end - - it 'shows top right search form' do - expect(page).to have_selector('#search') - end - - it 'contains location badge in top right search form' do - expect(page).to have_selector('.has-location-badge') - end - - context 'clicking the search field', js: true do - it 'shows category search dropdown' do - page.find('#search').click - - expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) - end - end - - context 'click the links in the category search dropdown', js: true do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } - - before do - page.find('#search').click - end - - it 'takes user to her issues page when issues assigned is clicked' do - find('.dropdown-menu').click_link 'Issues assigned to me' - - expect(page).to have_selector('.filtered-search') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her issues page when issues authored is clicked' do - find('.dropdown-menu').click_link "Issues I've created" - - expect(page).to have_selector('.filtered-search') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her MR page when MR assigned is clicked' do - find('.dropdown-menu').click_link 'Merge requests assigned to me' - - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'takes user to her MR page when MR authored is clicked' do - find('.dropdown-menu').click_link "Merge requests I've created" - - expect(page).to have_selector('.merge-requests-holder') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - end - - context 'entering text into the search field', js: true do - before do - page.within '.search-input-wrap' do - fill_in "search", with: project.name[0..3] - end - end - - it 'does not display the category search dropdown' do - expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) - end - end - end - end - - describe 'search for commits' do - let(:project) { create(:project, :repository) } - - before do - visit search_path(project_id: project.id) - end - - it 'redirects to commit page when search by sha and only commit found' do - fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f' - - click_button 'Search' - - expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) - end - - it 'redirects to single commit regardless of query case' do - fill_in 'search', with: '6D394385cf' - - click_button 'Search' - - expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f')) - end - - it 'holds on /search page when the only commit is found by message' do - create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master') - - fill_in 'search', with: 'deadbeef' - click_button 'Search' - - expect(page).to have_current_path('/search', only_path: true) - end - - it 'shows multiple matching commits' do - fill_in 'search', with: 'See merge request' - - click_button 'Search' - click_link 'Commits' - - expect(page).to have_selector('.commit-row-description', count: 9) - end - end - - context 'anonymous user' do - let(:project) { create(:project, :public) } - - before do - sign_out(user) - end - - it 'preserves the group being searched in' do - visit search_path(group_id: project.namespace.id) - - fill_in 'search', with: 'foo' - click_button 'Search' - - expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) - end - - it 'preserves the project being searched in' do - visit search_path(project_id: project.id) - - fill_in 'search', with: 'foo' - click_button 'Search' - - expect(find('#project_id', visible: false).value).to eq(project.id.to_s) - end - end -end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb new file mode 100644 index 00000000000..684af74d750 --- /dev/null +++ b/spec/finders/autocomplete_users_finder_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe AutocompleteUsersFinder do + describe '#execute' do + let!(:user1) { create(:user, username: 'johndoe') } + let!(:user2) { create(:user, :blocked, username: 'notsorandom') } + let!(:external_user) { create(:user, :external) } + let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } + let(:current_user) { create(:user) } + let(:params) { {} } + + let(:project) { nil } + let(:group) { nil } + + subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a } + + context 'when current_user not passed or nil' do + let(:current_user) { nil } + + it { is_expected.to match_array([]) } + end + + context 'when project passed' do + let(:project) { create(:project) } + + it { is_expected.to match_array([project.owner]) } + + context 'when author_id passed' do + let(:params) { { author_id: user2.id } } + + it { is_expected.to match_array([project.owner, user2]) } + end + end + + context 'when group passed and project not passed' do + let(:group) { create(:group, :public) } + + before do + group.add_users([user1], GroupMember::DEVELOPER) + end + + it { is_expected.to match_array([user1]) } + end + + it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) } + + context 'when filtered by search' do + let(:params) { { search: 'johndoe' } } + + it { is_expected.to match_array([user1]) } + end + + context 'when filtered by skip_users' do + let(:params) { { skip_users: [omniauth_user.id, current_user.id] } } + + it { is_expected.to match_array([user1, external_user]) } + end + + context 'when todos exist' do + let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) } + let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) } + let!(:done_todo1) { create(:todo, user: current_user, author: external_user, state: :done) } + let!(:done_todo2) { create(:todo, user: user1, author: external_user, state: :done) } + + context 'when filtered by todo_filter without todo_state_filter' do + let(:params) { { todo_filter: true } } + + it { is_expected.to match_array([]) } + end + + context 'when filtered by todo_filter with pending todo_state_filter' do + let(:params) { { todo_filter: true, todo_state_filter: 'pending' } } + + it { is_expected.to match_array([user1]) } + end + + context 'when filtered by todo_filter with done todo_state_filter' do + let(:params) { { todo_filter: true, todo_state_filter: 'done' } } + + it { is_expected.to match_array([external_user]) } + end + end + + context 'when filtered by current_user' do + let(:current_user) { user2 } + let(:params) { { current_user: true } } + + it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) } + end + + context 'when filtered by author_id' do + let(:params) { { author_id: user2.id } } + + it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) } + end + end +end diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index b3c9bca64cc..02415485d19 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -10,6 +10,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ @@ -38,6 +39,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index f8b37c0edde..f4b4d7980a4 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -271,12 +271,19 @@ describe('Fly out sidebar navigation', () => { }); it('sets transform of sub-items', () => { + const sidebar = document.createElement('div'); const subItems = el.querySelector('.sidebar-sub-level-items'); + + sidebar.style.width = '200px'; + + document.body.appendChild(sidebar); + + setSidebar(sidebar); showSubLevelItems(el); expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); + ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb new file mode 100644 index 00000000000..5c471cbdeda --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do + let!(:redirect_routes) { table(:redirect_routes) } + let!(:routes) { table(:routes) } + + before do + routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') + routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') + routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') + routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4') + routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5') + + # Valid redirects + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2') + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3') + + # Conflicting redirects + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') + end + + it 'deletes the conflicting redirect_routes in the range' do + expect(redirect_routes.count).to eq(8) + + expect do + described_class.new.perform(1, 3) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2) + + expect do + described_class.new.perform(4, 5) + end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0) + + expect(redirect_routes.count).to eq(3) + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 1bcdc369c44..3c8350b3aad 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do .to raise_error(RuntimeError, /Your database user is not allowed/) end end + + describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + before do + User.class_eval do + include EachBatch + end + end + + context 'with enough rows to bulk queue jobs more than once' do + before do + stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1) + end + + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in groups of buffer size 1' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]]) + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'with not enough rows to bulk queue jobs more than once' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + end + end + + it 'queues jobs in bulk all at once (big buffer size)' do + expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]], + ['FooJob', [id3, id3]]]) + + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2) + end + end + + context 'without specifying batch_size' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob') + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob') + end.to raise_error(StandardError, /does not have an ID/) + end + end + end + + describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do + context 'when the model has an ID column' do + let!(:id1) { create(:user).id } + let!(:id2) { create(:user).id } + let!(:id3) { create(:user).id } + + around do |example| + Timecop.freeze { example.run } + end + + before do + User.class_eval do + include EachBatch + end + end + + context 'with batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + end + end + end + + context 'without batch_size option' do + it 'queues jobs correctly' do + Sidekiq::Testing.fake! do + model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds) + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + end + end + end + end + + context "when the model doesn't have an ID column" do + it 'raises error (for now)' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) + end.to raise_error(StandardError, /does not have an ID/) + end + end + end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 8beebc10040..4fa30d8df8b 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1761,17 +1761,9 @@ describe Gitlab::Diff::PositionTracer do let(:merge_commit) do update_file_again_commit - committer = repository.user_to_committer(current_user) - - options = { - message: "Merge branches", - author: committer, - committer: committer - } - merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") project.commit(branch_name) end diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index e9c0209fe3b..d4d75b66659 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') } + let(:user) { Gitlab::Git::User.new('Jane Doe', 'janedoe@example.com', 'user-456') } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } @@ -18,7 +18,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(committer, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, @blankrev, @newrev, @ref) { } end end @@ -28,7 +28,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +40,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/user_spec.rb index b0ddbb51449..0ebcecb26c0 100644 --- a/spec/lib/gitlab/git/committer_spec.rb +++ b/spec/lib/gitlab/git/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::Committer do +describe Gitlab::Git::User do let(:name) { 'Jane Doe' } let(:email) { 'janedoe@example.com' } let(:gl_id) { 'user-123' } diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 08010c2d0e2..8dc83a6db7f 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#base_and_descendants' do @@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#all_groups' do @@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index d5f4da3ce36..dbcc200b90b 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe Gitlab::OAuth::AuthHash do + let(:provider) { 'ldap'.freeze } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - provider: provider_ascii, + provider: provider, uid: uid_ascii, info: info_hash ) @@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" } - let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) } let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) } let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) } let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) } @@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) } let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) } - let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) } let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) } let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } @@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do end context 'defaults' do - it { expect(auth_hash.provider).to eql provider_utf8 } + it { expect(auth_hash.provider).to eq provider } it { expect(auth_hash.uid).to eql uid_utf8 } it { expect(auth_hash.email).to eql email_utf8 } it { expect(auth_hash.username).to eql nickname_utf8 } diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb new file mode 100644 index 00000000000..1df2477da51 --- /dev/null +++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170907170235_delete_conflicting_redirect_routes') + +describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do + let!(:redirect_routes) { table(:redirect_routes) } + let!(:routes) { table(:routes) } + + around do |example| + Timecop.freeze { example.run } + end + + before do + stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2) + stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2) + + routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') + routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') + routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') + routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4') + routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5') + + # Valid redirects + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2') + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3') + + # Conflicting redirects + redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4') + redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) + expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + expect do + migrate! + end.to change { redirect_routes.count }.from(8).to(3) + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 95da97b7bc5..77f0be6b120 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1439,4 +1439,24 @@ describe Ci::Pipeline, :mailer do it_behaves_like 'not sending any notification' end end + + describe '#latest_builds_with_artifacts' do + let!(:pipeline) { create(:ci_pipeline, :success) } + + let!(:build) do + create(:ci_build, :success, :artifacts, pipeline: pipeline) + end + + it 'returns the latest builds' do + expect(pipeline.latest_builds_with_artifacts).to eq([build]) + end + + it 'memoizes the returned relation' do + query_count = ActiveRecord::QueryRecorder + .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } } + .count + + expect(query_count).to eq(1) + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 11e64a0f877..e3cfa149e3a 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -207,11 +207,6 @@ eos context 'of a merge commit' do let(:repository) { project.repository } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test message', committer: author, author: author } - end - let(:merge_request) do create(:merge_request, source_branch: 'video', @@ -224,7 +219,7 @@ eos merge_commit_id = repository.merge(user, merge_request.diff_head_sha, merge_request, - commit_options) + 'Test message') repository.commit(merge_commit_id) end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 9c99c3e5c08..fadc8bfeb61 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -140,18 +140,6 @@ describe GpgKey do end end - describe 'notification', :mailer do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:gpg_key, user: user) - end - - should_email(user) - end - end - describe '#revoke' do it 'invalidates all associated gpg signatures and destroys the key' do gpg_key = create :gpg_key diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 96baeaff0a4..dbc4aba8547 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -169,16 +169,4 @@ describe Key, :mailer do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end end - - describe 'notification' do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:key, user: user) - end - - should_email(user) - end - end end diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 5faab9ba38b..be07ca2d945 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -6,7 +6,8 @@ describe PipelinesEmailService, :mailer do end let(:project) { create(:project, :repository) } - let(:recipient) { 'test@gitlab.com' } + let(:recipients) { 'test@gitlab.com' } + let(:receivers) { [recipients] } let(:data) do Gitlab::DataBuilder::Pipeline.build(pipeline) @@ -48,18 +49,24 @@ describe PipelinesEmailService, :mailer do shared_examples 'sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end end it 'sends email' do - should_only_email(double(notification_email: recipient), kind: :bcc) + emails = receivers.map { |r| double(notification_email: r) } + + should_only_email(*emails, kind: :bcc) end end shared_examples 'not sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end @@ -75,10 +82,6 @@ describe PipelinesEmailService, :mailer do subject.test(data) end - before do - subject.recipients = recipient - end - context 'when pipeline is failed' do before do data[:object_attributes][:status] = 'failed' @@ -104,10 +107,6 @@ describe PipelinesEmailService, :mailer do end context 'with recipients' do - before do - subject.recipients = recipient - end - context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' @@ -152,9 +151,7 @@ describe PipelinesEmailService, :mailer do end context 'with empty recipients list' do - before do - subject.recipients = ' ,, ' - end + let(:recipients) { ' ,, ' } context 'with failed pipeline' do before do @@ -165,5 +162,19 @@ describe PipelinesEmailService, :mailer do it_behaves_like 'not sending email' end end + + context 'with recipients list separating with newlines' do + let(:recipients) { "\ntest@gitlab.com, \r\nexample@gitlab.com" } + let(:receivers) { %w[test@gitlab.com example@gitlab.com] } + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48fc77423ff..78226c6c3fa 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2682,4 +2682,60 @@ describe Project do end end end + + describe '#latest_successful_builds_for' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + context 'without a ref' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for + end + end + + context 'with the ref set to the default branch' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for(project.default_branch) + end + end + + context 'with a ref that is not the default branch' do + it 'returns the latest successful pipeline for the given ref' do + expect(project.pipelines).to receive(:latest_successful_for).with('foo') + + project.latest_successful_pipeline_for('foo') + end + end + end + + describe '#latest_successful_pipeline_for_default_branch' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + it 'memoizes and returns the latest successful pipeline for the default branch' do + pipeline = double(:pipeline) + + expect(project.pipelines).to receive(:latest_successful_for) + .with(project.default_branch) + .and_return(pipeline) + .once + + 2.times do + expect(project.latest_successful_pipeline_for_default_branch) + .to eq(pipeline) + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7065d467ec0..60cd7e70055 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -8,12 +8,9 @@ describe Repository, models: true do let(:repository) { project.repository } let(:broken_repository) { create(:project, :broken_storage).repository } let(:user) { create(:user) } - let(:committer) { Gitlab::Git::Committer.from_user(user) } + let(:git_user) { Gitlab::Git::User.from_gitlab(user) } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test message', committer: author, author: author } - end + let(:message) { 'Test message' } let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) @@ -21,7 +18,7 @@ describe Repository, models: true do merge_commit_id = repository.merge(user, merge_request.diff_head_sha, merge_request, - commit_options) + message) repository.commit(merge_commit_id) end @@ -886,7 +883,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -932,20 +929,20 @@ describe Repository, models: true do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref) + .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end it 'runs without errors' do expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - service = Gitlab::Git::OperationService.new(committer, repository.raw_repository) + service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository) expect(service).to receive(:update_autocrlf_option) @@ -956,7 +953,7 @@ describe Repository, models: true do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end @@ -974,7 +971,7 @@ describe Repository, models: true do expect(target_project.repository.raw_repository).to receive(:fetch_ref) .and_call_original - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch( 'master', start_repository: project.repository.raw_repository, @@ -990,7 +987,7 @@ describe Repository, models: true do it 'does not fetch_ref and just pass the commit' do expect(target_repository).not_to receive(:fetch_ref) - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } end end @@ -1009,7 +1006,7 @@ describe Repository, models: true do end expect do - Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository, &:itself) @@ -1031,7 +1028,7 @@ describe Repository, models: true do repository.add_branch(user, branch, old_rev) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.not_to raise_error @@ -1049,7 +1046,7 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.to raise_error(Gitlab::Git::CommitError) @@ -1061,7 +1058,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) @@ -1287,10 +1284,7 @@ describe Repository, models: true do describe '#merge' do let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) } - let(:commit_options) do - author = repository.user_to_committer(user) - { message: 'Test \r\n\r\n message', committer: author, author: author } - end + let(:message) { 'Test \r\n\r\n message' } it 'merges the code and returns the commit id' do expect(merge_commit).to be_present @@ -1298,19 +1292,19 @@ describe Repository, models: true do end it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do - merge_commit_id = merge(repository, user, merge_request, commit_options) + merge_commit_id = merge(repository, user, merge_request, message) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end it 'removes carriage returns from commit message' do - merge_commit_id = merge(repository, user, merge_request, commit_options) + merge_commit_id = merge(repository, user, merge_request, message) - expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r")) + expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) end - def merge(repository, user, merge_request, options = {}) - repository.merge(user, merge_request.diff_head_sha, merge_request, options) + def merge(repository, user, merge_request, message) + repository.merge(user, merge_request.diff_head_sha, merge_request, message) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9602584f546..92e7d797cbd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -54,9 +54,9 @@ describe API::Projects do shared_examples_for 'projects response without N + 1 queries' do it 'avoids N + 1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + control = ActiveRecord::QueryRecorder.new do get api('/projects', current_user) - end.count + end if defined?(additional_project) additional_project @@ -66,7 +66,7 @@ describe API::Projects do expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control_count + 8) + end.not_to exceed_query_limit(control).with_threshold(8) end end diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb new file mode 100644 index 00000000000..7a604c0cadd --- /dev/null +++ b/spec/services/deploy_keys/create_service_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe DeployKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:deploy_key) } + + subject { described_class.new(user, params) } + + it "creates a deploy key" do + expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1) + end +end diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb new file mode 100644 index 00000000000..20382a3a618 --- /dev/null +++ b/spec/services/gpg_keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe GpgKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:gpg_key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a gpg key' do + expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1) + end +end diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb new file mode 100644 index 00000000000..bcb436c1e46 --- /dev/null +++ b/spec/services/keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Keys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a key' do + expect { subject.execute }.to change { user.keys.where(params).count }.by(1) + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2af2485eeed..64e676f22a0 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -150,9 +150,7 @@ describe MergeRequests::RefreshService do context 'manual merge of source branch' do before do # Merge master -> feature branch - author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } - commit_options = { message: 'Test message', committer: author, author: author } - @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options) + @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message') commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 3e493148b32..f4b36eb7eeb 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -84,7 +84,6 @@ describe NotificationService, :mailer do let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } - it { should_email(key.user) } describe 'never emails the ghost user' do let(:key_options) { { user: User.ghost } } diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 92cc9a37795..c551083ac90 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -57,6 +57,21 @@ describe Projects::UpdateService, '#execute' do end end end + + context 'When project visibility is higher than parent group' do + let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + before do + project.update(namespace: group, visibility_level: group.visibility_level) + end + + it 'does not update project visibility level' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) + expect(project.reload).to be_internal + end + end end describe 'when updating project that has forks' do @@ -148,7 +163,7 @@ describe Projects::UpdateService, '#execute' do result = update_project(project, admin, path: 'existing') expect(result).to include(status: :error) - expect(result[:message]).to match('Project could not be updated!') + expect(result[:message]).to match('There is already a repository with that name on disk') expect(project).not_to be_valid expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') @@ -159,8 +174,10 @@ describe Projects::UpdateService, '#execute' do it 'returns an error result when record cannot be updated' do result = update_project(project, admin, { name: 'foo&bar' }) - expect(result).to eq({ status: :error, - message: 'Project could not be updated!' }) + expect(result).to eq({ + status: :error, + message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." + }) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ff1754fbe7e..92735336572 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ require './spec/simplecov_env' SimpleCovEnv.start! -ENV["RAILS_ENV"] ||= 'test' +ENV["RAILS_ENV"] = 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' require File.expand_path("../../config/environment", __FILE__) diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb new file mode 100644 index 00000000000..5b6d9c1a4df --- /dev/null +++ b/spec/support/matchers/navigation_matcher.rb @@ -0,0 +1,6 @@ +RSpec::Matchers.define :have_active_navigation do |expected| + match do |page| + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected) + end +end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb index 55b531b4cf7..ba0b805caad 100644 --- a/spec/support/query_recorder.rb +++ b/spec/support/query_recorder.rb @@ -34,15 +34,47 @@ RSpec::Matchers.define :exceed_query_limit do |expected| supports_block_expectations match do |block| - query_count(&block) > expected + query_count(&block) > expected_count + threshold end failure_message_when_negated do |actual| - "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + threshold_message = threshold > 0 ? " (+#{@threshold})" : '' + counts = "#{expected_count}#{threshold_message}" + "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" + end + + def with_threshold(threshold) + @threshold = threshold + self + end + + def threshold + @threshold.to_i + end + + def expected_count + if expected.is_a?(ActiveRecord::QueryRecorder) + expected.count + else + expected + end + end + + def actual_count + @recorder.count end def query_count(&block) @recorder = ActiveRecord::QueryRecorder.new(&block) @recorder.count end + + def log_message + if expected.is_a?(ActiveRecord::QueryRecorder) + extra_queries = (expected.log - @recorder.log).join("\n\n") + "Extra queries: \n\n #{extra_queries}" + else + @recorder.log_message + end + end end diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb new file mode 100644 index 00000000000..c92c7f603d6 --- /dev/null +++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb @@ -0,0 +1,21 @@ +shared_examples 'issuable user dropdown behaviors' do + include FilteredSearchHelpers + + before do + issuable # ensure we have at least one issuable + sign_in(user_in_dropdown) + end + + %w[author assignee].each do |dropdown| + describe "#{dropdown} dropdown", :js do + it 'only includes members of the project/group' do + visit issuables_path + + filtered_search.set("#{dropdown}:") + + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) + end + end + end +end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index 639b0924197..639b0924197 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb diff --git a/spec/support/shared_examples/features/search_shared_examples.rb b/spec/support/shared_examples/features/search_shared_examples.rb new file mode 100644 index 00000000000..25ebbf011d5 --- /dev/null +++ b/spec/support/shared_examples/features/search_shared_examples.rb @@ -0,0 +1,5 @@ +shared_examples 'top right search form' do + it 'does not show top right search form' do + expect(page).not_to have_selector('.search') + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 71b9deeabc3..6e5b9700b54 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -3,6 +3,8 @@ require 'rspec/mocks' module TestEnv extend self + ComponentFailedToInstallError = Class.new(StandardError) + # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => '2d1096e', @@ -63,6 +65,11 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) + unless Rails.env.test? + puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n" + exit 1 + end + # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -122,50 +129,23 @@ module TestEnv end def setup_gitlab_shell - puts "\n==> Setting up Gitlab Shell..." - start = Time.now - gitlab_shell_dir = Gitlab.config.gitlab_shell.path - shell_needs_update = component_needs_update?(gitlab_shell_dir, - Gitlab::Shell.version_required) - - unless !shell_needs_update || system('rake', 'gitlab:shell:install') - puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n" - FileUtils.rm_rf(gitlab_shell_dir) - exit 1 - end - - puts " GitLab Shell setup in #{Time.now - start} seconds...\n" + component_timed_setup('GitLab Shell', + install_dir: Gitlab.config.gitlab_shell.path, + version: Gitlab::Shell.version_required, + task: 'gitlab:shell:install') end def setup_gitaly - puts "\n==> Setting up Gitaly..." - start = Time.now socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) - if gitaly_dir_stale?(gitaly_dir) - puts " Gitaly is outdated, cleaning up #{gitaly_dir}!" - FileUtils.rm_rf(gitaly_dir) - end - - gitaly_needs_update = component_needs_update?(gitaly_dir, - Gitlab::GitalyClient.expected_server_version) + component_timed_setup('Gitaly', + install_dir: gitaly_dir, + version: Gitlab::GitalyClient.expected_server_version, + task: "gitlab:gitaly:install[#{gitaly_dir}]") do - unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") - puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n" - FileUtils.rm_rf(gitaly_dir) - exit 1 + start_gitaly(gitaly_dir) end - - start_gitaly(gitaly_dir) - puts " Gitaly setup in #{Time.now - start} seconds...\n" - end - - def gitaly_dir_stale?(dir) - gitaly_executable = File.join(dir, 'gitaly') - return false unless File.exist?(gitaly_executable) - - File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION')) end def start_gitaly(gitaly_dir) @@ -320,6 +300,40 @@ module TestEnv end end + def component_timed_setup(component, install_dir:, version:, task:) + puts "\n==> Setting up #{component}..." + start = Time.now + + ensure_component_dir_name_is_correct!(component, install_dir) + + if component_needs_update?(install_dir, version) + # Cleanup the component entirely to ensure we start fresh + FileUtils.rm_rf(install_dir) + unless system('rake', task) + raise ComponentFailedToInstallError + end + end + + yield if block_given? + + rescue ComponentFailedToInstallError + puts "\n#{component} failed to install, cleaning up #{install_dir}!\n" + FileUtils.rm_rf(install_dir) + exit 1 + ensure + puts " #{component} setup in #{Time.now - start} seconds...\n" + end + + def ensure_component_dir_name_is_correct!(component, path) + actual_component_dir_name = File.basename(path) + expected_component_dir_name = component.parameterize + + unless actual_component_dir_name == expected_component_dir_name + puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n" + exit 1 + end + end + def component_needs_update?(component_folder, expected_version) version = File.read(File.join(component_folder, 'VERSION')).strip diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 6f9ddb6c63c..f7b67b8efc6 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -31,8 +31,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id, :gc, lease_key, lease_uuid) end @@ -77,8 +77,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end diff --git a/yarn.lock b/yarn.lock index de4a9ac4487..02917125f05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,14 +49,7 @@ ajv-keywords@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" -ajv@^4.7.0: - version "4.11.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^4.9.1: +ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: @@ -64,11 +57,11 @@ ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.1.5: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486" + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" dependencies: co "^4.6.0" - fast-deep-equal "^0.1.0" + fast-deep-equal "^1.0.0" json-schema-traverse "^0.3.0" json-stable-stringify "^1.0.1" @@ -877,7 +870,7 @@ backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" -balanced-match@^0.4.1, balanced-match@^0.4.2: +balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -983,14 +976,7 @@ bootstrap-sass@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1" -brace-expansion@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -brace-expansion@^1.1.8: +brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" dependencies: @@ -1712,7 +1698,7 @@ decompress-response@^3.2.0: dependencies: mimic-response "^1.0.0" -deep-equal@^1.0.1: +deep-equal@^1.0.1, deep-equal@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -1730,7 +1716,14 @@ default-require-extensions@^1.0.0: dependencies: strip-bom "^2.0.0" -defined@^1.0.0: +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +defined@^1.0.0, defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -2037,6 +2030,24 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.5.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2: version "0.10.24" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14" @@ -2429,9 +2440,9 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" -fast-deep-equal@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" fast-levenshtein@~2.0.4: version "2.0.6" @@ -2564,6 +2575,12 @@ follow-redirects@^1.2.3: dependencies: debug "^2.4.5" +for-each@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" + dependencies: + is-function "~1.0.0" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -2574,6 +2591,10 @@ for-own@^0.1.4: dependencies: for-in "^0.1.5" +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -2646,6 +2667,10 @@ function-bind@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +function-bind@^1.1.1, function-bind@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -2720,7 +2745,7 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.1: +glob@^7.0.0, glob@^7.1.1, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2742,11 +2767,7 @@ glob@^7.0.3, glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: - version "9.14.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" - -globals@^9.14.0: +globals@^9.0.0, globals@^9.14.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -2888,7 +2909,7 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" -has@^1.0.1: +has@^1.0.1, has@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" dependencies: @@ -3163,6 +3184,14 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + is-dotfile@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" @@ -3201,6 +3230,10 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-function@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -3276,6 +3309,12 @@ is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + is-relative@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" @@ -3302,6 +3341,10 @@ is-svg@^2.0.0: dependencies: html-comment-regex "^1.1.0" +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -4087,7 +4130,7 @@ minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.1.3, minimist@^1.2.0: +minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -4225,8 +4268,8 @@ node-libs-browser@^2.0.0: vm-browserify "0.0.4" node-pre-gyp@^0.6.36: - version "0.6.36" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + version "0.6.37" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" dependencies: mkdirp "^0.5.1" nopt "^4.0.1" @@ -4235,6 +4278,7 @@ node-pre-gyp@^0.6.36: request "^2.81.0" rimraf "^2.6.1" semver "^5.3.0" + tape "^4.6.3" tar "^2.2.1" tar-pack "^3.4.0" @@ -4356,6 +4400,14 @@ object-component@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" +object-inspect@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d" + +object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -5383,6 +5435,12 @@ resolve@^1.1.6, resolve@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" +resolve@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" + dependencies: + path-parse "^1.0.5" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -5390,6 +5448,12 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" +resumer@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" + dependencies: + through "~2.3.4" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -5805,6 +5869,14 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string.prototype.trim@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -5896,6 +5968,24 @@ tapable@^0.2.7: version "0.2.8" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" +tape@^4.6.3: + version "4.8.0" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" + dependencies: + deep-equal "~1.0.1" + defined "~1.0.0" + for-each "~0.3.2" + function-bind "~1.1.0" + glob "~7.1.2" + has "~1.0.1" + inherits "~2.0.3" + minimist "~1.2.0" + object-inspect "~1.3.0" + resolve "~1.4.0" + resumer "~0.0.0" + string.prototype.trim "~1.1.2" + through "~2.3.8" + tar-pack@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" @@ -5943,7 +6033,7 @@ three@^0.84.0: version "0.84.0" resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" -through@2, through@^2.3.6, through@~2.3, through@~2.3.1: +through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" |