diff options
80 files changed, 642 insertions, 248 deletions
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/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/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/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/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/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/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/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/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/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..910e0d8b5ec 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,7 +77,7 @@ #{ _('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 @@ -89,7 +89,7 @@ #{ _('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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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_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/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/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/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 |