diff options
133 files changed, 2189 insertions, 477 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index b7db5a33faf..0cac769bd55 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -39,5 +39,6 @@ If applicable, any groups/projects that are happy to have this feature turned on - [ ] Cross post chatops slack command to `#support_gitlab-com` and in your team channel - [ ] Announce on the issue that the flag has been enabled - [ ] Remove feature flag and add changelog entry +- [ ] After the flag removal is deployed, [clean up the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel /label ~"feature flag" diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 80a6ab9598a..7254c50a568 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -87,7 +87,6 @@ export default { :file="file" :show-tooltip="true" :show-staged-icon="true" - :force-modified-icon="true" /> <new-dropdown :type="file.type" diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 838447e6c75..90c764587a3 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -45,6 +45,11 @@ export default { required: false, default: () => false, }, + singleEmbed: { + type: Boolean, + required: false, + default: false, + }, thresholds: { type: Array, required: false, @@ -240,7 +245,10 @@ export default { </script> <template> - <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div + class="prometheus-graph col-12" + :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]" + > <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 587392adbc3..dfeeba238ca 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -10,9 +10,9 @@ import { } from '@gitlab/ui'; import _ from 'underscore'; import { mapActions, mapState } from 'vuex'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import { getParameterValues } from '~/lib/utils/url_utility'; +import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import MonitorAreaChart from './charts/area.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; @@ -168,8 +168,11 @@ export default { 'multipleDashboardsEnabled', 'additionalPanelTypesEnabled', ]), + firstDashboard() { + return this.allDashboards[0] || {}; + }, selectedDashboardText() { - return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); + return this.currentDashboard || this.firstDashboard.display_name; }, addingMetricsAvailable() { return IS_EE && this.canAddMetrics && !this.showEmptyState; @@ -258,6 +261,14 @@ export default { getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, + generateLink(group, title, yLabel) { + const dashboard = this.currentDashboard || this.firstDashboard.path; + const params = { dashboard, group, title, y_label: yLabel }; + return mergeUrlParams(params, window.location.href); + }, // TODO: END hideAddMetricModal() { this.$refs.addMetricModal.hide(); @@ -435,6 +446,7 @@ export default { <panel-type v-for="(graphData, graphIndex) in groupData.metrics" :key="`panel-type-${graphIndex}`" + :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :graph-data="graphData" :dashboard-width="elWidth" :index="`${index}-${graphIndex}`" @@ -475,6 +487,15 @@ export default { {{ __('Download CSV') }} </gl-dropdown-item> <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text=" + generateLink(groupData.group, graphData.title, graphData.y_label) + " + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> + <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}-${graphIndex}`" > diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index 9e85b0633fe..e3256147618 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -36,12 +36,15 @@ export default { }, computed: { ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), - groupData() { - const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length); - if (groupsWithData.length) { - return groupsWithData[0]; - } - return null; + charts() { + const groupWithMetrics = this.groups.find(group => + group.metrics.find(chart => this.chartHasData(chart)), + ) || { metrics: [] }; + + return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart)); + }, + isSingleChart() { + return this.charts.length === 1; }, }, mounted() { @@ -66,10 +69,8 @@ export default { 'setFeatureFlags', 'setShowErrorBanner', ]), - chartsWithData(charts) { - return charts.filter(chart => - chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), - ); + chartHasData(chart) { + return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)); }, onSidebarMutation() { setTimeout(() => { @@ -89,16 +90,17 @@ export default { }; </script> <template> - <div class="metrics-embed"> - <div v-if="groupData" class="row w-100 m-n2 pb-4"> + <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }"> + <div v-if="charts.length" class="row w-100 m-n2 pb-4"> <monitor-area-chart - v-for="graphData in chartsWithData(groupData.metrics)" + v-for="graphData in charts" :key="graphData.title" :graph-data="graphData" :container-width="elWidth" group-id="monitor-area-chart" :project-path="null" :show-border="true" + :single-embed="isSingleChart" /> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 3fbac71f3d7..96f62bc85ee 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -1,6 +1,7 @@ <script> import { mapState } from 'vuex'; import _ from 'underscore'; +import { __ } from '~/locale'; import { GlDropdown, GlDropdownItem, @@ -28,6 +29,10 @@ export default { GlTooltip: GlTooltipDirective, }, props: { + clipboardText: { + type: String, + required: true, + }, graphData: { type: Object, required: true, @@ -76,6 +81,9 @@ export default { isPanelType(type) { return this.graphData.type && this.graphData.type === type; }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, }, }; </script> @@ -116,6 +124,13 @@ export default { <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> {{ __('Download CSV') }} </gl-dropdown-item> + <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text="clipboardText" + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> {{ __('Alerts') }} </gl-dropdown-item> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c0fee1ebb99..51cef20455c 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,9 +1,12 @@ import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue'; import store from './stores'; +Vue.use(GlToast); + export default (props = {}) => { const el = document.getElementById('prometheus-graphs'); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 627d37bac68..a223a8f5b08 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + canDisableEmails: { + type: Boolean, + required: false, + default: false, + }, canChangeVisibilityLevel: { type: Boolean, required: false, @@ -104,6 +109,7 @@ export default { lfsEnabled: true, requestAccessEnabled: true, highlightChangesClass: false, + emailsDisabled: false, }; return { ...defaults, ...this.currentSettings }; @@ -341,5 +347,14 @@ export default { /> </project-setting-row> </div> + <project-setting-row v-if="canDisableEmails" class="mb-3"> + <label class="js-emails-disabled"> + <input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" /> + <input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }} + </label> + <span class="form-text text-muted">{{ + __('This setting will override user notification preferences for all project members.') + }}</span> + </project-setting-row> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index cdf2d1020ba..beb2ac09992 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -26,11 +26,6 @@ export default { required: false, default: false, }, - forceModifiedIcon: { - type: Boolean, - required: false, - default: false, - }, size: { type: Number, required: false, @@ -48,8 +43,6 @@ export default { // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; - if (this.forceModifiedIcon) return `file-modified${suffix}`; - return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { @@ -88,7 +81,7 @@ export default { v-gl-tooltip.right :title="tooltipTitle" :class="{ 'ml-auto': isCentered }" - class="file-changed-icon" + class="file-changed-icon d-inline-block" > <icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" /> </span> diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e3dd127366d..96f6d02a68f 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -43,6 +43,7 @@ @extend .alert; background-color: $orange-100; color: $orange-900; + cursor: default; margin: 0; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5e65084a110..af6644b8fcc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar include SessionlessAuthentication + include ConfirmEmailWarning before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb new file mode 100644 index 00000000000..5a4b5897a4f --- /dev/null +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ConfirmEmailWarning + extend ActiveSupport::Concern + + included do + before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } + end + + protected + + def set_confirm_warning + return unless current_user + return if current_user.confirmed? + return if peek_request? || json_request? || !request.get? + + email = current_user.unconfirmed_email || current_user.email + + flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % { + email: email, + resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post), + update_link: view_context.link_to(_('Update it'), profile_path) + } + end +end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 2ae500a2fdf..b192189ba3c 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -11,7 +11,7 @@ class ConfirmationsController < Devise::ConfirmationsController protected def after_resending_confirmation_instructions_path_for(resource) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? stored_location_for(resource) || dashboard_projects_path : users_almost_there_path end def after_confirmation_path_for(resource_name, resource) diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb index c8facea1d70..e4093bed0ef 100644 --- a/app/controllers/projects/starrers_controller.rb +++ b/app/controllers/projects/starrers_controller.rb @@ -5,23 +5,9 @@ class Projects::StarrersController < Projects::ApplicationController def index @starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute - - # Normally the number of public starrers is equal to the number of visible - # starrers. We need to fix the counts in two cases: when the current user - # is an admin (and can see everything) and when the current user has a - # private profile and has starred the project (and can see itself). - @public_count = - if @current_user&.admin? - @starrers.with_public_profile.count - elsif @current_user&.private_profile && has_starred_project?(@starrers) - @starrers.size - 1 - else - @starrers.size - end - - @total_count = @project.starrers.size + @public_count = @project.starrers.with_public_profile.size + @total_count = @project.starrers.size @private_count = @total_count - @public_count - @sort = params[:sort].presence || sort_value_name @starrers = @starrers.sort_by_attribute(@sort).page(params[:page]) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index db10515c0b4..e773ec09924 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -69,12 +69,12 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?)) - user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path + confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path end def after_inactive_sign_up_path_for(resource) Gitlab::AppLogger.info(user_created_message) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? dashboard_projects_path : users_almost_there_path end private @@ -135,4 +135,12 @@ class RegistrationsController < Devise::RegistrationsController def terms_accepted? Gitlab::Utils.to_boolean(params[:terms_opt_in]) end + + def confirmed_or_unconfirmed_access_allowed(user) + user.confirmed? || Feature.enabled?(:soft_email_confirmation) + end + + def stored_location_or_dashboard(user) + stored_location_for(user) || dashboard_projects_path + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 160f9ac4793..bd26bd01313 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,6 +31,11 @@ module GroupsHelper can?(current_user, :change_share_with_group_lock, group) end + def can_disable_group_emails?(group) + Feature.enabled?(:emails_disabled, group, default_enabled: true) && + can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled? + end + def group_issues_count(state:) IssuesFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 11b9cf22142..5678304ffcf 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -5,7 +5,7 @@ module NotificationsHelper def notification_icon_class(level) case level.to_sym - when :disabled + when :disabled, :owner_disabled 'microphone-slash' when :participating 'volume-up' @@ -18,6 +18,16 @@ module NotificationsHelper end end + def notification_icon_level(notification_setting, emails_disabled = false) + if emails_disabled + 'owner_disabled' + elsif notification_setting.global? + current_user.global_notification_setting.level + else + notification_setting.level + end + end + def notification_icon(level, text = nil) icon("#{notification_icon_class(level)} fw", text: text) end @@ -53,6 +63,8 @@ module NotificationsHelper _('Use your global notification setting') when :custom _('You will only receive notifications for the events you choose') + when :owner_disabled + _('Notifications have been disabled by the project or group owner') end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 71c9c121e48..33bf2d57fae 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -155,6 +155,12 @@ module ProjectsHelper end end + def can_disable_emails?(project, current_user) + return false if project.group&.emails_disabled? + + can?(current_user, :set_emails_disabled, project) && Feature.enabled?(:emails_disabled, project, default_enabled: true) + end + def last_push_event current_user&.recent_push(@project) end @@ -541,13 +547,15 @@ module ProjectsHelper snippetsAccessLevel: feature.snippets_access_level, pagesAccessLevel: feature.pages_access_level, containerRegistryEnabled: !!project.container_registry_enabled, - lfsEnabled: !!project.lfs_enabled + lfsEnabled: !!project.lfs_enabled, + emailsDisabled: project.emails_disabled? } end def project_permissions_panel_data(project) { currentSettings: project_permissions_settings(project), + canDisableEmails: can_disable_emails?(project, current_user), canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), allowedVisibilityOptions: project_allowed_visibility_levels(project), visibilityHelpPath: help_page_path('public_access/public_access'), diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3b28eb246db..0a943a33bbb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -328,6 +328,10 @@ module Ci config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) end + def self.bridgeable_statuses + ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending] + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb index dab034b7234..5556fc8d3f0 100644 --- a/app/models/clusters/clusters_hierarchy.rb +++ b/app/models/clusters/clusters_hierarchy.rb @@ -46,7 +46,7 @@ module Clusters def group_clusters_base_query group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id') - join_sources = ::Group.left_joins(:clusters).join_sources + join_sources = ::Group.left_joins(:clusters).arel.join_sources model .unscoped @@ -59,7 +59,7 @@ module Clusters def project_clusters_base_query projects = ::Project.arel_table project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id') - join_sources = ::Project.left_joins(:clusters).join_sources + join_sources = ::Project.left_joins(:clusters).arel.join_sources model .unscoped diff --git a/app/models/repository.rb b/app/models/repository.rb index 9d45a12fa6e..6f63cd32da4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -389,11 +389,15 @@ class Repository expire_statistics_caches end - # Runs code after a repository has been created. - def after_create + def expire_status_cache expire_exists_cache expire_root_ref_cache expire_emptiness_caches + end + + # Runs code after a repository has been created. + def after_create + expire_status_cache repository_event(:create_repository) end diff --git a/app/models/user.rb b/app/models/user.rb index 374e00987c5..6131a8dc710 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1507,6 +1507,13 @@ class User < ApplicationRecord super end + # override from Devise::Confirmable + def confirmation_period_valid? + return false if Feature.disabled?(:soft_email_confirmation) + + super + end + private def default_private_profile_to_false diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 943c707218d..6e91317eb20 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -18,6 +18,7 @@ class DeploymentEntity < Grape::Entity end expose :created_at + expose :finished_at expose :tag expose :last? expose :user, using: UserEntity diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb index 04db6b88489..3fd3e1b9cc8 100644 --- a/app/serializers/deployment_serializer.rb +++ b/app/serializers/deployment_serializer.rb @@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer entity DeploymentEntity def represent_concise(resource, opts = {}) - opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]] + opts[:only] = [:iid, :id, :sha, :created_at, :finished_at, :tag, :last?, :id, ref: [:name]] represent(resource, opts) end end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 61de3c93337..c02fd024345 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -98,6 +98,10 @@ class IssuableSidebarBasicEntity < Grape::Entity autocomplete_projects_path(project_id: issuable.project.id) end + expose :project_emails_disabled do |issuable| + issuable.project.emails_disabled? + end + private def current_user diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index 1db18fcf401..3fd38444196 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -8,8 +8,6 @@ module Git PROCESS_COMMIT_LIMIT = 100 def execute - project.repository.after_create if project.empty_repo? - create_events create_pipelines execute_project_hooks @@ -70,11 +68,11 @@ module Git end def enqueue_invalidate_cache - ProjectCacheWorker.perform_async( - project.id, - invalidated_file_types, - [:commit_count, :repository_size] - ) + file_types = invalidated_file_types + + return unless file_types.present? + + ProjectCacheWorker.perform_async(project.id, file_types, [], false) end def base_params diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 431a5aedf2e..d2b037a680c 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -83,8 +83,16 @@ module Git # Schedules processing of commit messages def enqueue_process_commit_messages - limited_commits.each do |commit| - next unless commit.matches_cross_reference_regex? + referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) + + upstream_commit_ids = upstream_commit_ids(referencing_commits) + + referencing_commits.each do |commit| + # Avoid reprocessing commits that already exist upstream if the project + # is a fork. This will prevent duplicated/superfluous system notes on + # mentionables referenced by a commit that is pushed to the upstream, + # that is then also pushed to forks when these get synced by users. + next if upstream_commit_ids.include?(commit.id) ProcessCommitWorker.perform_async( project.id, @@ -142,5 +150,18 @@ module Git def branch_name strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) } end + + def upstream_commit_ids(commits) + set = Set.new + + upstream_project = project.fork_source + if upstream_project + upstream_project + .commits_by(oids: commits.map(&:id)) + .each { |commit| set << commit.id } + end + + set + end end end diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 4daf3683eaf..e50d2b8e994 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,4 +1,5 @@ - can_create_subgroups = can?(current_user, :create_subgroup, @group) +- emails_disabled = @group.emails_disabled? .group-home-panel .row.mb-3 @@ -21,7 +22,7 @@ .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .group-buttons - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled - if can? current_user, :create_projects, @group - new_project_label = _("New project") - new_subgroup_label = _("New subgroup") diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index d3375e00bad..94a938021f9 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -11,13 +11,20 @@ .form-check = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' = f.label :share_with_group_lock, class: 'form-check-label' do - %span + %span.d-block - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %br %span.descr.text-muted= share_with_group_lock_help_text(@group) + .form-group.append-bottom-default + .form-check + = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' + = f.label :emails_disabled, class: 'form-check-label' do + %span.d-block= s_('GroupSettings|Disable email notifications') + %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') + = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group + = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index cf17ee44145..1776d260e19 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -1,16 +1,15 @@ +- emails_disabled = group.emails_disabled? + .gl-responsive-table-row.notification-list-item .table-section.section-40 %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to group.name, group_path(group) .table-section.section-30.text-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled .table-section.section-30 = form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f| diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index 823fec3fab4..63a77b335b6 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -1,12 +1,11 @@ +- emails_disabled = project.emails_disabled? + %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to_project(project) .float-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 824fe3c791d..4783b10cf6d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,8 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 +- emails_disabled = @project.emails_disabled? + .project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] } .row.append-bottom-8 .home-panel-title-row.col-md-12.col-lg-6.d-flex @@ -41,7 +43,7 @@ .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .d-inline-flex - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled .count-buttons.d-inline-flex = render 'projects/buttons/star' diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b4f8377c008..825088a58e7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -137,7 +137,11 @@ .js-sidebar-participants-entry-point - if signed_in - .js-sidebar-subscriptions-entry-point + - if issuable_sidebar[:project_emails_disabled] + .block.js-emails-disabled + = notification_description(:owner_disabled) + - else + .js-sidebar-subscriptions-entry-point - project_ref = issuable_sidebar[:reference] .block.project-reference diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 749aa258af6..b4266937a4e 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,6 +1,15 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - aria_label = button_title + - btn_class << " disabled" + - else + - button_title = _("Notification setting") + - aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = hidden_setting_source_input(notification_setting) @@ -8,14 +17,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } .float-left = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml index 052e6da5bae..3c8cc023848 100644 --- a/app/views/shared/notifications/_new_button.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -1,6 +1,13 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - btn_class << " disabled" + - else + - button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f| = hidden_setting_source_input(notification_setting) @@ -9,14 +16,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" } = sprite_icon("arrow-down", css_class: "icon mr-0") .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden = sprite_icon("arrow-down", css_class: "icon") diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 622bd6f1f48..61d34981458 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -42,10 +42,8 @@ class PostReceive user = identify_user(post_received) return false unless user - # Expire the branches cache so we have updated data for this push - post_received.project.repository.expire_branches_cache if post_received.includes_branches? - # We only need to expire tags once per push - post_received.project.repository.expire_caches_for_tags if post_received.includes_tags? + # We only need to expire certain caches once per push + expire_caches(post_received) post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| service_klass = @@ -74,6 +72,30 @@ class PostReceive after_project_changes_hooks(post_received, user, refs.to_a, changes) end + # Expire the project, branch, and tag cache once per push. Schedule an + # update for the repository size and commit count if necessary. + def expire_caches(post_received) + project = post_received.project + + project.repository.expire_status_cache if project.empty_repo? + project.repository.expire_branches_cache if post_received.includes_branches? + project.repository.expire_caches_for_tags if post_received.includes_tags? + + enqueue_repository_cache_update(post_received) + end + + def enqueue_repository_cache_update(post_received) + stats_to_invalidate = [:repository_size] + stats_to_invalidate << :commit_count if post_received.includes_default_branch? + + ProjectCacheWorker.perform_async( + post_received.project.id, + [], + stats_to_invalidate, + true + ) + end + def after_project_changes_hooks(post_received, user, refs, changes) hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 3efb5343a96..f6ebe4ab006 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -2,7 +2,8 @@ # Worker for processing individual commit messages pushed to a repository. # -# Jobs for this worker are scheduled for every commit that is being pushed. As a +# Jobs for this worker are scheduled for every commit that contains mentionable +# references in its message and does not exist in the upstream project. As a # result of this the workload of this worker should be kept to a bare minimum. # Consider using an extra worker if you need to add any extra (and potentially # slow) processing of commits. @@ -19,7 +20,6 @@ class ProcessCommitWorker project = Project.find_by(id: project_id) return unless project - return if commit_exists_in_upstream?(project, commit_hash) user = User.find_by(id: user_id) @@ -77,17 +77,4 @@ class ProcessCommitWorker Commit.from_hash(hash, project) end - - private - - # Avoid reprocessing commits that already exist in the upstream - # when project is forked. This will also prevent duplicated system notes. - def commit_exists_in_upstream?(project, commit_hash) - upstream_project = project.fork_source - - return false unless upstream_project - - commit_id = commit_hash.with_indifferent_access[:id] - upstream_project.commit(commit_id).present? - end end diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 4e8ea903139..5ac860c93e0 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -12,13 +12,15 @@ class ProjectCacheWorker # CHANGELOG. # statistics - An Array containing columns from ProjectStatistics to # refresh, if empty all columns will be refreshed + # refresh_statistics - A boolean that determines whether project statistics should + # be updated. # rubocop: disable CodeReuse/ActiveRecord - def perform(project_id, files = [], statistics = []) + def perform(project_id, files = [], statistics = [], refresh_statistics = true) project = Project.find_by(id: project_id) return unless project - update_statistics(project, statistics) + update_statistics(project, statistics) if refresh_statistics return unless project.repository.exists? diff --git a/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml b/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml new file mode 100644 index 00000000000..d93e7634ae5 --- /dev/null +++ b/changelogs/unreleased/10972-be-allow-restricting-group-members-by-a-domain-whitelist-ce.yml @@ -0,0 +1,5 @@ +--- +title: Add new table to store email domain per group +merge_request: 31071 +author: +type: added diff --git a/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml b/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml new file mode 100644 index 00000000000..a5fe7b1d18e --- /dev/null +++ b/changelogs/unreleased/50020-fe-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml @@ -0,0 +1,5 @@ +--- +title: UI for disabling group/project email notifications +merge_request: 30961 +author: Dustin Spicuzza +type: added diff --git a/changelogs/unreleased/50070-legacy-attachments.yml b/changelogs/unreleased/50070-legacy-attachments.yml new file mode 100644 index 00000000000..03f1cec0f67 --- /dev/null +++ b/changelogs/unreleased/50070-legacy-attachments.yml @@ -0,0 +1,5 @@ +--- +title: Create rake tasks for migrating legacy uploads out of deprecated paths +merge_request: 29409 +author: +type: other diff --git a/changelogs/unreleased/56130-deployment-date.yml b/changelogs/unreleased/56130-deployment-date.yml new file mode 100644 index 00000000000..7d1e84bbaa4 --- /dev/null +++ b/changelogs/unreleased/56130-deployment-date.yml @@ -0,0 +1,5 @@ +--- +title: Add finished_at to the internal API Deployment entity +merge_request: 31808 +author: +type: other diff --git a/changelogs/unreleased/61335-fix-file-icon-status.yml b/changelogs/unreleased/61335-fix-file-icon-status.yml new file mode 100644 index 00000000000..d524d91b246 --- /dev/null +++ b/changelogs/unreleased/61335-fix-file-icon-status.yml @@ -0,0 +1,5 @@ +--- +title: Fix IDE new files icon in tree +merge_request: 31560 +author: +type: fixed diff --git a/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml b/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml new file mode 100644 index 00000000000..b6bc03f4003 --- /dev/null +++ b/changelogs/unreleased/62971-embed-specific-metrics-chart-in-issue.yml @@ -0,0 +1,5 @@ +--- +title: Embed specific metrics chart in issue +merge_request: 31644 +author: +type: added diff --git a/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml b/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml new file mode 100644 index 00000000000..1caa5fa84ce --- /dev/null +++ b/changelogs/unreleased/66023-starrers-count-do-not-match-after-searching.yml @@ -0,0 +1,5 @@ +--- +title: Fix starrers counts after searching +merge_request: 31823 +author: +type: fixed diff --git a/changelogs/unreleased/dm-process-commit-worker-n-1.yml b/changelogs/unreleased/dm-process-commit-worker-n-1.yml new file mode 100644 index 00000000000..0bd7de6730a --- /dev/null +++ b/changelogs/unreleased/dm-process-commit-worker-n-1.yml @@ -0,0 +1,5 @@ +--- +title: Look up upstream commits once before queuing ProcessCommitWorkers +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/fix-commits-api-empty-refname.yml b/changelogs/unreleased/fix-commits-api-empty-refname.yml new file mode 100644 index 00000000000..efdb950e45d --- /dev/null +++ b/changelogs/unreleased/fix-commits-api-empty-refname.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 errors in commits api caused by empty ref_name parameter +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/optimize-note-indexes.yml b/changelogs/unreleased/optimize-note-indexes.yml new file mode 100644 index 00000000000..bfb84779abf --- /dev/null +++ b/changelogs/unreleased/optimize-note-indexes.yml @@ -0,0 +1,5 @@ +--- +title: Optimize DB indexes for ES indexing of notes +merge_request: 31846 +author: +type: performance diff --git a/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml b/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml new file mode 100644 index 00000000000..f412ba11b91 --- /dev/null +++ b/changelogs/unreleased/rf-remove-group-overview-security-dashboard-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove Security Dashboard feature flag +merge_request: 31820 +author: +type: other diff --git a/changelogs/unreleased/sh-post-receive-cache-clear-once.yml b/changelogs/unreleased/sh-post-receive-cache-clear-once.yml new file mode 100644 index 00000000000..b677adf78d9 --- /dev/null +++ b/changelogs/unreleased/sh-post-receive-cache-clear-once.yml @@ -0,0 +1,5 @@ +--- +title: Expire project caches once per push instead of once per ref +merge_request: 31876 +author: +type: performance diff --git a/changelogs/unreleased/tr-embed-metric-links.yml b/changelogs/unreleased/tr-embed-metric-links.yml new file mode 100644 index 00000000000..6918114a4ae --- /dev/null +++ b/changelogs/unreleased/tr-embed-metric-links.yml @@ -0,0 +1,5 @@ +--- +title: Generate shareable link for specific metric charts +merge_request: 31339 +author: +type: added diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb index 3dd12c7e64d..8ef9ff6b7fc 100644 --- a/config/initializers/8_devise.rb +++ b/config/initializers/8_devise.rb @@ -81,7 +81,7 @@ Devise.setup do |config| # You can use this to let your user access some features of your application # without confirming the account, but blocking it after a certain period # (ie 2 days). - # config.allow_unconfirmed_access_for = 2.days + config.allow_unconfirmed_access_for = 30.days # Defines which key will be used when confirming an account # config.confirmation_keys = [ :email ] diff --git a/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb new file mode 100644 index 00000000000..c6c5b56ed8b --- /dev/null +++ b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateAllowedEmailDomainsForGroups < ActiveRecord::Migration[5.2] + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + create_table :allowed_email_domains do |t| + t.timestamps_with_timezone null: false + t.references :group, references: :namespace, + column: :group_id, + type: :integer, + null: false, + index: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + t.string :domain, null: false, limit: 255 + end + end +end diff --git a/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb b/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb new file mode 100644 index 00000000000..cbbece35901 --- /dev/null +++ b/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AddIndexNotesOnProjectIdAndIdAndSystemFalse < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(*index_arguments) + end + + def down + remove_concurrent_index(*index_arguments) + end + + private + + def index_arguments + [ + :notes, + [:project_id, :id], + { + name: 'index_notes_on_project_id_and_id_and_system_false', + where: 'NOT system' + } + ] + end +end diff --git a/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb b/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb new file mode 100644 index 00000000000..158c88e6258 --- /dev/null +++ b/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class RemoveIndexNotesOnNoteableType < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index(*index_arguments) + end + + def down + add_concurrent_index(*index_arguments) + end + + private + + def index_arguments + [ + :notes, + [:noteable_type], + { + name: 'index_notes_on_noteable_type' + } + ] + end +end diff --git a/db/schema.rb b/db/schema.rb index cf7f0fc0a3d..ce5fd38129a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_12_070645) do +ActiveRecord::Schema.define(version: 2019_08_15_093949) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -26,6 +26,14 @@ ActiveRecord::Schema.define(version: 2019_08_12_070645) do t.integer "cached_markdown_version" end + create_table "allowed_email_domains", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "group_id", null: false + t.string "domain", limit: 255, null: false + t.index ["group_id"], name: "index_allowed_email_domains_on_group_id" + end + create_table "analytics_cycle_analytics_group_stages", force: :cascade do |t| t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false @@ -2279,7 +2287,7 @@ ActiveRecord::Schema.define(version: 2019_08_12_070645) do t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" - t.index ["noteable_type"], name: "index_notes_on_noteable_type" + t.index ["project_id", "id"], name: "index_notes_on_project_id_and_id_and_system_false", where: "(NOT system)" t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type" t.index ["review_id"], name: "index_notes_on_review_id" end @@ -3670,6 +3678,7 @@ ActiveRecord::Schema.define(version: 2019_08_12_070645) do t.index ["type"], name: "index_web_hooks_on_type" end + add_foreign_key "allowed_email_domains", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "start_event_label_id", on_delete: :cascade add_foreign_key "analytics_cycle_analytics_group_stages", "namespaces", column: "group_id", on_delete: :cascade diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index c2ac063ce37..16a193550a1 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -72,12 +72,12 @@ our AsciiDoc snippets, wikis and repos using delimited blocks: - **Markdown** - ````markdown + ~~~markdown ```plantuml Bob -> Alice : hello Alice -> Bob : Go Away ``` - ```` + ~~~ - **AsciiDoc** diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 47abbc512e0..306d611f6bf 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -309,6 +309,12 @@ GraphQL queries are recorded in that file. For example: {"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration":7} ``` +## `migrations.log` + +Introduced in GitLab 12.3. This file lives in `/var/log/gitlab/gitlab-rails/migrations.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/migrations.log` for +installations from source. + ## Reconfigure Logs Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md index fd8ea8d3162..86e8b763f51 100644 --- a/doc/administration/raketasks/uploads/migrate.md +++ b/doc/administration/raketasks/uploads/migrate.md @@ -103,3 +103,13 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[NamespaceFileUploader, S sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeRequest]" ``` + +## Migrate legacy uploads out of deprecated paths + +> Introduced in GitLab 12.3. + +To migrate all uploads created by legacy uploaders, run: + +```shell +bundle exec rake gitlab:uploads:legacy:migrate +``` diff --git a/doc/api/README.md b/doc/api/README.md index b7ee710b87a..9156d719e11 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -77,11 +77,12 @@ authentication is not provided. For those cases where it is not required, this will be mentioned in the documentation for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). -There are three ways to authenticate with the GitLab API: +There are four ways to authenticate with the GitLab API: 1. [OAuth2 tokens](#oauth2-tokens) 1. [Personal access tokens](#personal-access-tokens) 1. [Session cookie](#session-cookie) +1. [GitLab CI job token](#gitlab-ci-job-token-premium) **(PREMIUM)** For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available: @@ -151,6 +152,14 @@ The primary user of this authentication method is the web frontend of GitLab its which can use the API as the authenticated user to get a list of their projects, for example, without needing to explicitly pass an access token. +### GitLab CI job token **(PREMIUM)** + +With a few API endpoints you can use a [GitLab CI job token](../user/project/new_ci_build_permissions_model.md#job-token) +to authenticate with the API: + +* [Get job artifacts](jobs.md#get-job-artifacts) +* [Pipeline triggers](pipeline_triggers.md) + ### Impersonation tokens > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md index ced4344a0b0..cb8d383f7d9 100644 --- a/doc/ci/multi_project_pipelines.md +++ b/doc/ci/multi_project_pipelines.md @@ -176,6 +176,21 @@ Upstream pipelines take precedence over downstream ones. If there are two variables with the same name defined in both upstream and downstream projects, the ones defined in the upstream project will take precedence. +### Mirroring status from upstream pipeline + +You can mirror the pipeline status from an upstream pipeline to a bridge job by +using the `needs:pipeline` keyword. The latest pipeline status from master is +replicated to the bridge job. + +Example: + +```yaml +upstream_bridge: + stage: test + needs: + pipeline: other/project +``` + ### Limitations Because bridge jobs are a little different to regular jobs, it is not diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 680f2cd13c2..c1e3eb9680b 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -655,15 +655,16 @@ nicely on different mobile devices. ## Code blocks -- Always wrap code added to a sentence in inline code blocks (``` ` ```). +- Always wrap code added to a sentence in inline code blocks (`` ` ``). E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`. File names, commands, entries, and anything that refers to code should be added to code blocks. To make things easier for the user, always add a full code block for things that can be useful to copy and paste, as they can easily do it with the button on code blocks. +- Add a blank line above and below code blocks. - For regular code blocks, always use a highlighting class corresponding to the language for better readability. Examples: - ````md + ~~~md ```ruby Ruby code ``` @@ -673,16 +674,17 @@ nicely on different mobile devices. ``` ```md - Markdown code + [Markdown code example](example.md) ``` ```text - Code for which no specific highlighting class is available. + Code or text for which no specific highlighting class is available. ``` - ```` + ~~~ -- To display raw markdown instead of rendered markdown, use four backticks on their own lines around the - markdown to display. See [example](https://gitlab.com/gitlab-org/gitlab-ce/blob/8c1991b9bb7e3b8d606481fdea316d633cfa5eb7/doc/development/documentation/styleguide.md#L275-287). +- To display raw markdown instead of rendered markdown, you can use triple backticks + with `md`, like the `Markdown code` example above, unless you want to include triple + backticks in the code block as well. In that case, use triple tildes (`~~~`) instead. - For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## Alert boxes @@ -1024,7 +1026,7 @@ on this document. Further explanation is given below. The following can be used as a template to get started: -````md +~~~md ## Descriptive title One or two sentence description of what endpoint does. @@ -1052,7 +1054,7 @@ Example response: } ] ``` -```` +~~~ ### Fake tokens @@ -1080,7 +1082,7 @@ You can use the following fake tokens as examples. ### Method description Use the following table headers to describe the methods. Attributes should -always be in code blocks using backticks (``` ` ```). +always be in code blocks using backticks (`` ` ``). ```md | Attribute | Type | Required | Description | diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index c97e179910b..67f36eb1ab4 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -216,3 +216,4 @@ bundle exec rake routes Since these take some time to create, it's often helpful to save the output to a file for quick reference. + diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 1c80fc543af..eee05eaef02 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -333,6 +333,10 @@ curl --request PUT localhost:9200/gitlab-production/_settings --data '{ Enable Elasticsearch search in **Admin > Settings > Integrations**. That's it. Enjoy it! +### Index limit + +Currently for repository and snippet files, GitLab would only index up to 1 MB of content, in order to avoid indexing timeout. + ## GitLab Elasticsearch Rake Tasks There are several rake tasks available to you via the command line: diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index 0e5bdcd9c79..c80f2f264b2 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -22,11 +22,12 @@ similarly mitigated by a rate limit. ## Admin Area settings -See -[User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md). +- [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md). +- [Rate limits on raw endpoints](../user/admin_area/settings/rate_limits_on_raw_endpoints.md) ## Rack Attack initializer This method of rate limiting is cumbersome, but has some advantages. It allows throttling of specific paths, and is also integrated into Git and container registry requests. See [Rack Attack initializer](rack_attack.md). + diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 95220d6364c..9c1258fa1aa 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -816,7 +816,7 @@ To configure your application variables: 1. Create a CI Variable, ensuring the key is prefixed with `K8S_SECRET_`. For example, you can create a variable with key -`K8S_SECRET_RAILS_MASTER_KEY`. + `K8S_SECRET_RAILS_MASTER_KEY`. 1. Run an Auto Devops pipeline either by manually creating a new pipeline or by pushing a code change to GitLab. @@ -1017,10 +1017,10 @@ Everything behaves the same way, except: - It's enabled by setting the `INCREMENTAL_ROLLOUT_MODE` variable to `timed`. - Instead of the standard `production` job, the following jobs with a 5 minute delay between each are created: - 1. `timed rollout 10%` - 1. `timed rollout 25%` - 1. `timed rollout 50%` - 1. `timed rollout 100%` + 1. `timed rollout 10%` + 1. `timed rollout 25%` + 1. `timed rollout 50%` + 1. `timed rollout 100%` ## Currently supported languages diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md index 423ba1cfbd7..1218465c87a 100644 --- a/doc/university/training/end-user/README.md +++ b/doc/university/training/end-user/README.md @@ -9,12 +9,8 @@ which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-tr through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides) project. ---- - ## Git Intro ---- - ### What is a Version Control System (VCS) - Records changes to a file @@ -22,8 +18,6 @@ project. - Disaster Recovery - Types of VCS: Local, Centralized and Distributed ---- - ### Short Story of Git - 1991-2002: The Linux kernel was being maintained by sharing archived files @@ -31,8 +25,6 @@ project. - 2002: The Linux kernel project began using a DVCS called BitKeeper - 2005: BitKeeper revoked the free-of-charge status and Git was created ---- - ### What is Git - Distributed Version Control System @@ -42,8 +34,6 @@ project. - Disaster recovery friendly - Open Source ---- - ### Getting Help - Use the tools at your disposal when you get stuck. @@ -51,14 +41,10 @@ project. - Use Google (i.e. StackOverflow, Google groups) - Read documentation at <https://git-scm.com> ---- - ## Git Setup Workshop Time! ---- - ### Setup - Windows: Install 'Git for Windows' @@ -69,8 +55,6 @@ Workshop Time! - Debian: `sudo apt-get install git-all` - Red Hat `sudo yum install git-all` ---- - ### Configure - One-time configuration of the Git client: @@ -91,16 +75,12 @@ git config --global --list - You might want or be required to use an SSH key. - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html) ---- - ### Workspace - Choose a directory on you machine easy to access - Create a workspace or development directory - This is where we'll be working and adding content ---- - ```bash mkdir ~/development cd ~/development @@ -111,12 +91,8 @@ mkdir ~/workspace cd ~/workspace ``` ---- - ## Git Basics ---- - ### Git Workflow - Untracked files @@ -128,8 +104,6 @@ cd ~/workspace - Upstream - Hosted repository on a shared server ---- - ### GitLab - GitLab is an application to code, test and deploy. @@ -137,8 +111,6 @@ cd ~/workspace issue tracking, Merge Requests, and other features. - The hosted version of GitLab is gitlab.com ---- - ### New Project - Sign in into your gitlab.com account @@ -146,8 +118,6 @@ cd ~/workspace - Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git> - On your machine clone the `training-examples` project ---- - ### Git and GitLab basics 1. Edit `edit_this_file.rb` in `training-examples` @@ -158,8 +128,6 @@ cd ~/workspace 1. Push the commit to the remote 1. View the git log ---- - ```shell # Edit `edit_this_file.rb` git status @@ -170,8 +138,6 @@ git push origin master git log ``` ---- - ### Feature Branching 1. Create a new feature branch called `squash_some_bugs` @@ -179,8 +145,6 @@ git log 1. Commit 1. Push ---- - ```shell git checkout -b squash_some_bugs # Edit `bugs.rb` @@ -190,14 +154,8 @@ git commit -m 'Fix some buggy code' git push origin squash_some_bugs ``` ---- - ## Merge Request ---- - -### Merge requests - - When you want feedback create a merge request - Target is the ‘default’ branch (usually master) - Assign or mention the person you would like to review @@ -206,8 +164,6 @@ git push origin squash_some_bugs - Anyone can comment, not just the assignee - Push corrections to the same branch ---- - ### Merge request example - Create your first merge request @@ -216,8 +172,6 @@ git push origin squash_some_bugs - Push a new commit to the same branch - Review the changes again and notice the update ---- - ### Feedback and Collaboration - Merge requests are a time for feedback and collaboration @@ -230,24 +184,17 @@ git push origin squash_some_bugs --- -- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review) +- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: + [Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review) - See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) ---- - ## Merge Conflicts ---- - -### Merge Conflicts - - Happen often - Learning to fix conflicts is hard - Practice makes perfect - Force push after fixing conflicts. Be careful! ---- - ### Example Plan 1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'. @@ -261,8 +208,6 @@ git push origin squash_some_bugs 1. Force push the changes 1. Finally continue with the Merge Request ---- - ### Example 1/2 ```sh @@ -282,8 +227,6 @@ git commit -am "add line6 and line7" git push origin master ``` ---- - ### Example 2/2 Create a merge request on the GitLab web UI. You'll see a conflict warning. @@ -305,8 +248,6 @@ git rebase --continue git push origin conflicts_branch -f ``` ---- - ### Notes - When to use `git merge` and when to use `git rebase` @@ -314,12 +255,8 @@ git push origin conflicts_branch -f - Merge when bringing changes from feature to master - Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing/> ---- - ## Revert and Unstage ---- - ### Unstage To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch: @@ -347,8 +284,6 @@ If we want to remove a file from the repository but keep it on disk, say we forg git rm <filename> --cache ``` ---- - ### Undo Commits Undo last commit putting everything back into the staging area: @@ -377,8 +312,6 @@ git reset --hard HEAD^^ Don't reset after pushing ---- - ### Reset Workflow 1. Edit file again 'edit_this_file.rb' @@ -392,8 +325,6 @@ Don't reset after pushing 1. Pull for updates 1. Push changes ---- - ```sh # Change file edit_this_file.rb git status @@ -407,8 +338,6 @@ git pull origin master git push origin master ``` ---- - ### git revert vs git reset Reset removes the commit while revert removes the changes but leaves the commit @@ -425,16 +354,10 @@ git revert <rev commit hash> # reverted commit is back (new commit created again) ``` ---- - ## Questions ---- - ## Instructor Notes ---- - ### Version Control - Local VCS was used with a filesystem or a simple db. diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md index 4178afa2086..24dc670d9d5 100644 --- a/doc/university/training/topics/bisect.md +++ b/doc/university/training/topics/bisect.md @@ -4,13 +4,11 @@ comments: false # Bisect -## Bisect - - Find a commit that introduced a bug - Works through a process of elimination - Specify a known good and bad revision to begin -## Bisect +## Bisect sample workflow 1. Start the bisect process 1. Enter the bad revision (usually latest commit) diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md index fa0cb5fe6a4..f5bcdfcbf12 100644 --- a/doc/university/training/topics/cherry_picking.md +++ b/doc/university/training/topics/cherry_picking.md @@ -4,13 +4,11 @@ comments: false # Cherry Pick -## Cherry Pick - - Given an existing commit on one branch, apply the change to another branch - Useful for backporting bug fixes to previous release branches - Make the commit on the master branch and pick in to stable -## Cherry Pick +## Cherry Pick sample workflow 1. Check out a new 'stable' branch from 'master' 1. Change back to 'master' @@ -19,8 +17,6 @@ comments: false 1. Check out the 'stable' branch 1. Cherry pick the commit using the SHA obtained earlier -## Commands - ```bash git checkout master git checkout -b stable diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md index d2efe634533..f530389d4da 100644 --- a/doc/university/training/topics/feature_branching.md +++ b/doc/university/training/topics/feature_branching.md @@ -11,15 +11,13 @@ comments: false - Push branches to the server frequently - Hint: This is a cheap backup for your work-in-progress code -## Feature branching +## Feature branching sample workflow 1. Create a new feature branch called 'squash_some_bugs' 1. Edit '`bugs.rb`' and remove all the bugs. 1. Commit 1. Push -## Commands - ```sh git checkout -b squash_some_bugs # Edit `bugs.rb` diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md index e8ff7916590..3fadb58e804 100644 --- a/doc/university/training/topics/getting_started.md +++ b/doc/university/training/topics/getting_started.md @@ -35,8 +35,6 @@ comments: false 1. Create a '`Workspace`' directory in your home directory. 1. Clone the '`training-examples`' project. -## Commands - ```sh mkdir ~/workspace cd ~/workspace @@ -69,8 +67,6 @@ Modified files that have been marked to go in the next commit. 1. Push the commit to the remote 1. View the git log -## Commands - ```sh # Edit `edit_this_file.rb` git status diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md index 7152fc2030b..0c9a50bb5e1 100644 --- a/doc/university/training/topics/git_add.md +++ b/doc/university/training/topics/git_add.md @@ -4,8 +4,6 @@ comments: false # Git Add -## Git Add - Adds content to the index or staging area. - Adds a list of file: @@ -20,8 +18,6 @@ Adds content to the index or staging area. git add -A ``` -## Git add continued - - Add all text files in current dir: ```bash diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md index dd235fe3a81..97bb038f405 100644 --- a/doc/university/training/topics/merge_conflicts.md +++ b/doc/university/training/topics/merge_conflicts.md @@ -9,7 +9,7 @@ comments: false - Practice makes perfect - Force push after fixing conflicts. Be careful! -## Merge conflicts +## Merge conflicts sample workflow 1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'. 1. Commit and push. @@ -22,8 +22,6 @@ comments: false 1. Force push the changes. 1. Finally continue with the Merge Request. -## Commands - ```sh git checkout -b conflicts_branch diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md index b5bbe7b2e1e..656871ae5b2 100644 --- a/doc/university/training/topics/merge_requests.md +++ b/doc/university/training/topics/merge_requests.md @@ -30,8 +30,6 @@ comments: false - Be as receptive as possible - Feedback is about the best code, not the person. You are not your code -## Feedback and Collaboration - Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: [https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review) diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md index 21abad88cfa..d3e63db0c6a 100644 --- a/doc/university/training/topics/stash.md +++ b/doc/university/training/topics/stash.md @@ -25,7 +25,7 @@ and we need to change to a different branch. git stash apply stash@{3} ``` -- Every time we save a stash it gets stacked so by using list we can see all our +- Every time we save a stash it gets stacked so by using `list` we can see all our stashes. ```sh @@ -54,7 +54,7 @@ and we need to change to a different branch. - If we meet conflicts we need to either reset or commit our changes. - Conflicts through `pop` will not drop a stash afterwards. -## Git Stash +## Git Stash sample workflow 1. Modify a file 1. Stage file @@ -64,8 +64,6 @@ and we need to change to a different branch. 1. Apply with pop 1. View list to confirm changes -## Commands - ```sh # Modify edit_this_file.rb file git add . diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md index cdbb8a2da7c..631b93cc384 100644 --- a/doc/university/training/topics/tags.md +++ b/doc/university/training/topics/tags.md @@ -11,18 +11,12 @@ type: reference - Many projects combine an annotated release tag with a stable branch - Consider setting deployment/release tags automatically -# Tags +## Tags sample workflow - Create a lightweight tag - Create an annotated tag - Push the tags to the remote repository -**Additional resources** - -<https://git-scm.com/book/en/Git-Basics-Tagging> - -# Commands - ```sh git checkout master @@ -36,6 +30,10 @@ git tag git push origin --tags ``` +**Additional resources** + +<https://git-scm.com/book/en/Git-Basics-Tagging> + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md index fa1f63f9ec4..d7482bf2bd5 100644 --- a/doc/university/training/topics/unstage.md +++ b/doc/university/training/topics/unstage.md @@ -4,8 +4,6 @@ comments: false # Unstage -## Unstage - - To remove files from stage use reset HEAD where HEAD is the last commit of the current branch. This will unstage the file but maintain the modifications. ```bash diff --git a/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png b/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png Binary files differnew file mode 100644 index 00000000000..c32eb93c8a8 --- /dev/null +++ b/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png diff --git a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md new file mode 100644 index 00000000000..b2d56be154b --- /dev/null +++ b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md @@ -0,0 +1,20 @@ +--- +type: reference +--- + +# Rate limits on raw endpoints **(CORE ONLY)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30829) in GitLab 12.2. + +This setting allows you to rate limit the requests to raw endpoints, defaults to `300` requests per minute. +It can be modified in **Admin Area > Network > Performance Optimization**. + +For example, requests over `300` per minute to `https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/controllers/application_controller.rb` will be blocked. + +![Rate limits on raw endpoints](img/rate_limits_on_raw_endpoints.png) + +This limit is: + +- Applied independently per project, per commit and per file path. +- Not applied per IP address. +- Active by default. To disable, set the option to `0`. diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md index df86b2a1cbe..862316b57da 100644 --- a/doc/user/asciidoc.md +++ b/doc/user/asciidoc.md @@ -277,11 +277,11 @@ source - a listing that is embellished with (colorized) syntax highlighting ---- ``` -````asciidoc +~~~asciidoc \```language fenced code - a shorthand syntax for the source block \``` -```` +~~~ ```asciidoc [,attribution,citetitle] diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 43fd0bfd45a..d1d4f3740b0 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -350,6 +350,38 @@ Restriction currently applies to UI, API access is not restricted. To avoid accidental lock-out, admins and group owners are are able to access the group regardless of the IP restriction. +#### Allowed domain restriction **(PREMIUM ONLY)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7297) in +[GitLab Premium](https://about.gitlab.com/pricing/) 12.2. + +You can restrict access to groups and their underlying projects by +allowing only users with email addresses in particular domains to be added to the group. + +Add email domains you want to whitelist and users with emails from different +domains won't be allowed to be added to this group. + +Some domains cannot be restricted. These are the most popular public email domains, such as: + +- `gmail.com` +- `yahoo.com` +- `hotmail.com` +- `aol.com` +- `msn.com` +- `hotmail.co.uk` +- `hotmail.fr` +- `live.com` +- `outlook.com` +- `icloud.com` + +To enable this feature: + +1. Navigate to the group's **Settings > General** page. +1. Expand the **Permissions, LFS, 2FA** section, and enter domain name into **Restrict membership by email** field. +1. Click **Save changes**. + +This will enable the domain-checking for all new users added to the group from this moment on. + #### Group file templates **(PREMIUM)** Group file templates allow you to share a set of templates for common file @@ -379,6 +411,17 @@ To enable this feature, navigate to the group settings page, expand the Define project templates at a group level by setting a group as the template source. [Learn more about group-level project templates](custom_project_templates.md). +#### Disabling email notifications + +You can disable all email notifications related to the group, which also includes +it's subgroups and projects. + +To enable this feature: + +1. Navigate to the group's **Settings > General** page. +1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**. +1. Click **Save changes**. + ### Advanced settings - **Projects**: View all projects within that group, add members to each project, diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 16684b9f72b..4fd7c5abf78 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -126,6 +126,7 @@ The following table depicts the various user permission levels in a project. | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Delete issues | | | | | ✓ | +| Disable notification emails | | | | | ✓ | | Force push to protected branches (*4*) | | | | | | | Remove protected branches (*4*) | | | | | | @@ -220,6 +221,7 @@ group. | Remove group | | | | | ✓ | | Delete group epic **(ULTIMATE)** | | | | | ✓ | | View group Audit Events | | | | | ✓ | +| Disable notification emails | | | | | ✓ | - (1): Groups can be set to [allow either Owners or Owners and Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup) diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 4c8bd230b3f..8a82b163481 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -488,15 +488,6 @@ without having to check the entire job log. [Read more about JUnit test reports](../../../ci/junit_test_reports.md). -## Live preview with Review Apps - -If you configured [Review Apps](https://about.gitlab.com/features/review-apps/) for your project, -you can preview the changes submitted to a feature-branch through a merge request -in a per-branch basis. No need to checkout the branch, install and preview locally; -all your changes will be available to preview by anyone with the Review Apps link. - -[Read more about Review Apps.](../../../ci/review_apps/index.md) - ## Merge request diff file navigation When reviewing changes in the **Changes** tab the diff can be navigated using diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 17ec9ecb5d1..4e3db95b6d6 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -32,6 +32,12 @@ links will be missing from the sidebar UI. You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see Issues or Merge Requests, both of which use Labels and Milestones, then you shouldn't be denied access to Labels and Milestones pages. +#### Disabling email notifications + +You can disable all email notifications related to the project by selecting the +**Disable email notifications** checkbox. Only the project owner is allowed to change +this setting. + ### Issue settings Add an [issue description template](../description_templates.md#description-templates) to your project, so that every new issue will start with a custom template. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index d82f7c6fdc7..ccb8844aea3 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -51,6 +51,10 @@ Organization like this is suitable for users that belong to different groups but same need for being notified for every group they are member of. These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. +The group owner can disable email notifications for a group, which also includes +it's subgroups and projects. If this is the case, you will not receive any corresponding notifications, +and the notification button will be disabled with an explanatory tooltip. + ### Project Settings ![notification settings](img/notification_project_settings.png) @@ -60,6 +64,10 @@ other setting. This is suitable for users that have different needs for notifications per project basis. These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. +The project owner (or it's group owner) can disable email notifications for the project. +If this is the case, you will not receive any corresponding notifications, and the notification +button will be disabled with an explanatory tooltip. + ## Notification events Below is the table of events users can be notified of: diff --git a/lib/api/commits.rb b/lib/api/commits.rb index e4f4e79cd46..a2f3e87ebd2 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -43,7 +43,7 @@ module API path = params[:path] before = params[:until] after = params[:since] - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all] + ref = params[:ref_name].presence || user_project.try(:default_branch) || 'master' unless params[:all] offset = (params[:page] - 1) * params[:per_page] all = params[:all] with_stats = params[:with_stats] diff --git a/lib/gitlab/background_migration/legacy_upload_mover.rb b/lib/gitlab/background_migration/legacy_upload_mover.rb new file mode 100644 index 00000000000..051c1176edb --- /dev/null +++ b/lib/gitlab/background_migration/legacy_upload_mover.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class takes a legacy upload and migrates it to the correct location + class LegacyUploadMover + include Gitlab::Utils::StrongMemoize + + attr_reader :upload, :project, :note + attr_accessor :logger + + def initialize(upload) + @upload = upload + @note = Note.find_by(id: upload.model_id) + @project = note&.project + @logger = Gitlab::BackgroundMigration::Logger.build + end + + def execute + return unless upload + + if !project + # if we don't have models associated with the upload we can not move it + warn('Deleting upload due to model not found.') + + destroy_legacy_upload + elsif note.is_a?(LegacyDiffNote) + return unless move_legacy_diff_file + + migrate_upload + elsif !legacy_file_exists? + warn('Deleting upload due to file not found.') + destroy_legacy_upload + else + migrate_upload + end + end + + private + + def migrate_upload + return unless copy_upload_to_project + + add_upload_link_to_note_text + destroy_legacy_file + destroy_legacy_upload + end + + # we should proceed and log whenever one upload copy fails, no matter the reasons + # rubocop: disable Lint/RescueException + def copy_upload_to_project + @uploader = FileUploader.copy_to(legacy_file_uploader, project) + + logger.info( + message: 'MigrateLegacyUploads: File copied successfully', + old_path: legacy_file_uploader.file.path, new_path: @uploader.file.path + ) + true + rescue Exception => e + warn( + 'File could not be copied to project uploads', + file_path: legacy_file_uploader.file.path, error: e.message + ) + false + end + # rubocop: enable Lint/RescueException + + def destroy_legacy_upload + if note + note.remove_attachment = true + note.save + end + + if upload.destroy + logger.info(message: 'MigrateLegacyUploads: Upload was destroyed.', upload: upload.inspect) + else + warn('MigrateLegacyUploads: Upload destroy failed.') + end + end + + def destroy_legacy_file + legacy_file_uploader.file.delete + end + + def add_upload_link_to_note_text + new_text = "#{note.note} \n #{@uploader.markdown_link}" + # Bypass validations because old data may have invalid + # noteable values. If we fail hard here, we may kill the + # entire background migration, which affects a range of notes. + note.update_attribute(:note, new_text) + end + + def legacy_file_uploader + strong_memoize(:legacy_file_uploader) do + uploader = upload.build_uploader + uploader.retrieve_from_store!(File.basename(upload.path)) + uploader + end + end + + def legacy_file_exists? + legacy_file_uploader.file.exists? + end + + # we should proceed and log whenever one upload copy fails, no matter the reasons + # rubocop: disable Lint/RescueException + def move_legacy_diff_file + old_path = upload.absolute_path + old_path_sub = '-/system/note/attachment' + + if !File.exist?(old_path) || !old_path.include?(old_path_sub) + log_legacy_diff_note_problem(old_path) + return false + end + + new_path = upload.absolute_path.sub(old_path_sub, '-/system/legacy_diff_note/attachment') + new_dir = File.dirname(new_path) + FileUtils.mkdir_p(new_dir) + + FileUtils.mv(old_path, new_path) + rescue Exception => e + log_legacy_diff_note_problem(old_path, new_path, e) + false + end + + def warn(message, params = {}) + logger.warn( + params.merge(message: "MigrateLegacyUploads: #{message}", upload: upload.inspect) + ) + end + + def log_legacy_diff_note_problem(old_path, new_path = nil, error = nil) + warn('LegacyDiffNote upload could not be moved to a new path', + old_path: old_path, new_path: new_path, error: error&.message + ) + end + # rubocop: enable Lint/RescueException + end + end +end diff --git a/lib/gitlab/background_migration/legacy_uploads_migrator.rb b/lib/gitlab/background_migration/legacy_uploads_migrator.rb new file mode 100644 index 00000000000..a9d38a27e0c --- /dev/null +++ b/lib/gitlab/background_migration/legacy_uploads_migrator.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This migration takes all legacy uploads (that were uploaded using AttachmentUploader) + # and migrate them to the new (FileUploader) location (=under projects). + # + # We have dependencies (uploaders) in this migration because extracting code would add a lot of complexity + # and possible errors could appear as the logic in the uploaders is not trivial. + # + # This migration will be removed in 13.0 in order to get rid of a migration that depends on + # the application code. + class LegacyUploadsMigrator + include Database::MigrationHelpers + + def perform(start_id, end_id) + Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader').find_each do |upload| + LegacyUploadMover.new(upload).execute + end + end + end + end +end diff --git a/lib/gitlab/background_migration/logger.rb b/lib/gitlab/background_migration/logger.rb new file mode 100644 index 00000000000..4ea89771eff --- /dev/null +++ b/lib/gitlab/background_migration/logger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Logger that can be used for migrations logging + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'migrations' + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index b0ce7457926..7ec03d132c0 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -57,7 +57,10 @@ module Gitlab end def bridge? - @attributes.to_h.dig(:options, :trigger).present? + attributes_hash = @attributes.to_h + attributes_hash.dig(:options, :trigger).present? || + (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) && + attributes_hash.dig(:options, :bridge_needs, :pipeline).present?) end def to_resource diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 998130e5bd0..2e1eab270ff 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -55,7 +55,8 @@ module Gitlab parallel: job[:parallel], instance: job[:instance], start_in: job[:start_in], - trigger: job[:trigger] + trigger: job[:trigger], + bridge_needs: job[:needs] }.compact }.compact end diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index 24d752b8a4b..2a8bcd015a8 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -39,6 +39,17 @@ module Gitlab end end + def includes_default_branch? + # If the branch doesn't have a default branch yet, we presume the + # first branch pushed will be the default. + return true unless project.default_branch.present? + + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.branch_ref?(ref) && + Gitlab::Git.branch_name(ref) == project.default_branch + end + end + private def deserialize_changes(changes) diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 038553c5dd7..1542905d2ce 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -100,9 +100,7 @@ module Gitlab .merge(services_usage) .merge(approximate_counts) }.tap do |data| - if Feature.enabled?(:group_overview_security_dashboard) - data[:counts][:user_preferences] = user_preferences_usage - end + data[:counts][:user_preferences] = user_preferences_usage end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake new file mode 100644 index 00000000000..18fb8afe455 --- /dev/null +++ b/lib/tasks/gitlab/uploads/legacy.rake @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +namespace :gitlab do + namespace :uploads do + namespace :legacy do + desc "GitLab | Uploads | Migrate all legacy attachments" + task migrate: :environment do + class Upload < ApplicationRecord + self.table_name = 'uploads' + + include ::EachBatch + end + + migration = 'LegacyUploadsMigrator'.freeze + batch_size = 5000 + delay_interval = 5.minutes.to_i + + Upload.where(uploader: 'AttachmentUploader').each_batch(of: batch_size) do |relation, index| + start_id, end_id = relation.pluck('MIN(id), MAX(id)').first + delay = index * delay_interval + + BackgroundMigrationWorker.perform_in(delay, migration, [start_id, end_id]) + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6227ca9afc3..e8d27360395 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3943,6 +3943,9 @@ msgstr "" msgid "Disable" msgstr "" +msgid "Disable email notifications" +msgstr "" + msgid "Disable for this project" msgstr "" @@ -5181,6 +5184,9 @@ msgstr "" msgid "Generate a default set of labels" msgstr "" +msgid "Generate link to chart" +msgstr "" + msgid "Generate new export" msgstr "" @@ -5478,6 +5484,9 @@ msgstr "" msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" msgstr "" +msgid "GroupSettings|Disable email notifications" +msgstr "" + msgid "GroupSettings|Learn more about badges." msgstr "" @@ -5505,6 +5514,9 @@ msgstr "" msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually." msgstr "" +msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects." +msgstr "" + msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group" msgstr "" @@ -6534,6 +6546,9 @@ msgid_plural "Limited to showing %d events at most" msgstr[0] "" msgstr[1] "" +msgid "Link copied to clipboard" +msgstr "" + msgid "Linked emails (%{email_count})" msgstr "" @@ -7586,6 +7601,9 @@ msgstr "" msgid "Notifications" msgstr "" +msgid "Notifications have been disabled by the project or group owner" +msgstr "" + msgid "Notifications off" msgstr "" @@ -8146,6 +8164,9 @@ msgstr "" msgid "Please add a list to your board first" msgstr "" +msgid "Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}." +msgstr "" + msgid "Please choose a group URL with no special characters." msgstr "" @@ -9528,6 +9549,9 @@ msgstr "" msgid "Resend invite" msgstr "" +msgid "Resend it" +msgstr "" + msgid "Reset health check access token" msgstr "" @@ -11649,6 +11673,9 @@ msgstr "" msgid "This setting can be overridden in each project." msgstr "" +msgid "This setting will override user notification preferences for all project members." +msgstr "" + msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}" msgstr "" @@ -12304,6 +12331,9 @@ msgstr "" msgid "Update failed" msgstr "" +msgid "Update it" +msgstr "" + msgid "Update now" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index c09c65a57a5..9ff7919f199 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + context 'Create', :quarantine do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/127 describe 'Web IDE file templates' do include Runtime::Fixtures diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb new file mode 100644 index 00000000000..0c598a360af --- /dev/null +++ b/spec/controllers/concerns/confirm_email_warning_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ConfirmEmailWarning do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + controller(ApplicationController) do + # `described_class` is not available in this context + include ConfirmEmailWarning # rubocop:disable RSpec/DescribedClass + + def index + head :ok + end + end + + RSpec::Matchers.define :set_confirm_warning_for do |email| + match do |response| + expect(response).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address.") + end + end + + describe 'confirm email flash warning' do + context 'when not signed in' do + let(:user) { create(:user, confirmed_at: nil) } + + before do + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when signed in' do + before do + sign_in(user) + end + + context 'with a confirmed user' do + let(:user) { create(:user) } + + before do + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'with an unconfirmed user' do + let(:user) { create(:user, confirmed_at: nil) } + + context 'when executing a peek request' do + before do + request.path = '/-/peek' + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a json request' do + before do + get :index, format: :json + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a post request' do + before do + post :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a get request' do + before do + get :index + end + + context 'with an unconfirmed email address present' do + let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') } + + it { is_expected.to set_confirm_warning_for(user.unconfirmed_email) } + end + + context 'without an unconfirmed email address present' do + it { is_expected.to set_confirm_warning_for(user.email) } + end + end + end + end + end +end diff --git a/spec/controllers/projects/starrers_controller_spec.rb b/spec/controllers/projects/starrers_controller_spec.rb index 59d258e99ce..7085cba08d5 100644 --- a/spec/controllers/projects/starrers_controller_spec.rb +++ b/spec/controllers/projects/starrers_controller_spec.rb @@ -3,23 +3,33 @@ require 'spec_helper' describe Projects::StarrersController do - let(:user) { create(:user) } - let(:private_user) { create(:user, private_profile: true) } + let(:user_1) { create(:user, name: 'John') } + let(:user_2) { create(:user, name: 'Michael') } + let(:private_user) { create(:user, name: 'Michael Douglas', private_profile: true) } let(:admin) { create(:user, admin: true) } - let(:project) { create(:project, :public, :repository) } + let(:project) { create(:project, :public) } before do - user.toggle_star(project) + user_1.toggle_star(project) + user_2.toggle_star(project) private_user.toggle_star(project) end describe 'GET index' do - def get_starrers - get :index, - params: { - namespace_id: project.namespace, - project_id: project - } + def get_starrers(search: nil) + get :index, params: { namespace_id: project.namespace, project_id: project, search: search } + end + + def user_ids + assigns[:starrers].map { |s| s['user_id'] } + end + + shared_examples 'starrers counts' do + it 'starrers counts are correct' do + expect(assigns[:total_count]).to eq(3) + expect(assigns[:public_count]).to eq(2) + expect(assigns[:private_count]).to eq(1) + end end context 'when project is public' do @@ -28,55 +38,118 @@ describe Projects::StarrersController do end context 'when no user is logged in' do + context 'with no searching' do + before do + get_starrers + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) + end + + include_examples 'starrers counts' + end + + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id) + end + + include_examples 'starrers counts' + end + end + + context 'when public user is logged in' do before do - get_starrers + sign_in(user_1) end - it 'only public starrers are visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id) - expect(user_ids).not_to include(private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'their star is also visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id) + end + + include_examples 'starrers counts' end end context 'when private user is logged in' do before do sign_in(private_user) - - get_starrers end - it 'their star is also visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id, private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'their star is also visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id, private_user.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id, private_user.id) + end + + include_examples 'starrers counts' end end context 'when admin is logged in' do before do sign_in(admin) - - get_starrers end - it 'all stars are visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id, private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'all users are visible' do + expect(user_ids).to include(user_1.id, user_2.id, private_user.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'public and private starrers are visible' do + expect(user_ids).to contain_exactly(user_2.id, private_user.id) + end + + include_examples 'starrers counts' end end end @@ -95,15 +168,14 @@ describe Projects::StarrersController do context 'when user is logged in' do before do sign_in(project.creator) - end - - it 'only public starrers are visible' do get_starrers + end - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id) - expect(user_ids).not_to include(private_user.id) + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) end + + include_examples 'starrers counts' end end end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index d05482f095e..fed4fc810f2 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -30,13 +30,36 @@ describe RegistrationsController do end context 'when send_user_confirmation_email is true' do - it 'does not authenticate user and sends confirmation email' do + before do stub_application_setting(send_user_confirmation_email: true) + end + + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end + + it 'does not authenticate the user and sends a confirmation email' do + post(:create, params: user_params) + + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(subject.current_user).to be_nil + end + end - post(:create, params: user_params) + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(subject.current_user).to be_nil + it 'authenticates the user and sends a confirmation email' do + post(:create, params: user_params) + + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(response).to redirect_to(dashboard_projects_path) + end end end diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index 6491b9dca19..b1b714277e4 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -8,7 +8,7 @@ FactoryBot.define do ref 'master' tag false created_at 'Di 29. Okt 09:50:00 CET 2013' - status :success + status :created pipeline factory: :ci_pipeline @@ -17,6 +17,7 @@ FactoryBot.define do end transient { downstream nil } + transient { upstream nil } after(:build) do |bridge, evaluator| bridge.project ||= bridge.pipeline.project @@ -26,6 +27,12 @@ FactoryBot.define do trigger: { project: evaluator.downstream.full_path } ) end + + if evaluator.upstream.present? + bridge.options = bridge.options.to_h.merge( + bridge_needs: { pipeline: evaluator.upstream.full_path } + ) + end end end end diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index 2ede92c8af0..3f6326114c9 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -56,10 +56,7 @@ FactoryBot.define do end trait :attachment_upload do - transient do - mount_point :attachment - end - + mount_point :attachment model { build(:note) } uploader "AttachmentUploader" end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 942a9889488..bcaed2a5f18 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -161,4 +161,27 @@ describe 'Group show page' do expect(find('.group-row:nth-child(3) .namespace-title > a')).to have_content(project3.title) end end + + context 'notification button', :js do + let(:maintainer) { create(:user) } + let!(:project) { create(:project, namespace: group) } + + before do + group.add_maintainer(maintainer) + sign_in(maintainer) + end + + it 'is enabled by default' do + visit path + + expect(page).to have_selector('.notifications-btn:not(.disabled)', visible: true) + end + + it 'is disabled if emails are disabled' do + group.update_attribute(:emails_disabled, true) + visit path + + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 832c4a57aa3..1e054a7b358 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -17,11 +17,10 @@ describe 'Invites' do group_invite.generate_invite_token! end - def confirm_email_and_sign_in(new_user) + def confirm_email(new_user) new_user_token = User.find_by_email(new_user.email).confirmation_token visit user_confirmation_path(confirmation_token: new_user_token) - fill_in_sign_in_form(new_user) end def fill_in_sign_up_form(new_user) @@ -155,17 +154,41 @@ describe 'Invites' do context 'email confirmation enabled' do let(:send_email_confirmation) { true } - it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do - fill_in_sign_up_form(new_user) - confirm_email_and_sign_in(new_user) + context 'when soft email confirmation is not enabled' do + before do + # stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end - expect(current_path).to eq(root_path) - expect(page).to have_content(project.full_name) - visit group_path(group) - expect(page).to have_content(group.full_name) + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + fill_in_sign_in_form(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end end - it "doesn't accept invitations until the user confirm his email" do + context 'when soft email confirmation is enabled' do + before do + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + end + + it "doesn't accept invitations until the user confirms his email" do fill_in_sign_up_form(new_user) sign_in(owner) @@ -176,11 +199,32 @@ describe 'Invites' do context 'the user sign-up using a different email address' do let(:invite_email) { build_stubbed(:user).email } - it 'signs up and redirects to the invitation page' do - fill_in_sign_up_form(new_user) - confirm_email_and_sign_in(new_user) + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end - expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + fill_in_sign_in_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end end end end diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index 7a721adc8dd..165d41950da 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -27,4 +27,14 @@ describe "User toggles subscription", :js do # Check we're unsubscribed. expect(subscription_button).to have_css("button:not(.is-checked)") end + + context 'when project emails are disabled' do + let(:project) { create(:project_empty_repo, :public, emails_disabled: true) } + + it 'is disabled' do + expect(page).to have_content('Notifications have been disabled by the project or group owner') + expect(page).to have_selector('.js-emails-disabled', visible: true) + expect(page).not_to have_selector('.js-issuable-subscribe-button') + end + end end diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index 1472cc882a7..d788c0574e2 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -20,4 +20,12 @@ describe 'User visits the notifications tab', :js do expect(page).to have_selector('#notifications-button', text: 'On mention') end + + context 'when project emails are disabled' do + let(:project) { create(:project, emails_disabled: true) } + + it 'notification button is disabled' do + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb index c19e46da913..6bd569e5ee2 100644 --- a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb +++ b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb @@ -3,18 +3,15 @@ require 'spec_helper' # This is a regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/37569 -# Quarantine: https://gitlab.com/gitlab-org/gitlab-ce/issues/65329 -describe 'Projects > Files > User browses a tree with a folder containing only a folder', :quarantine do +describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js do let(:project) { create(:project, :empty_repo) } let(:user) { project.owner } before do - # We need to disable the tree.flat_path provided by Gitaly to reproduce the issue - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) - project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder') sign_in(user) visit(project_tree_path(project, project.repository.root_ref)) + wait_for_requests end it 'shows the nested folder on a single row' do diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 46fd676954d..0e757e647a0 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -59,6 +59,12 @@ describe 'Projects > Settings > Visibility settings', :js do end end end + + context 'disable email notifications' do + it 'is visible' do + expect(page).to have_selector('.js-emails-disabled', visible: true) + end + end end context 'as maintainer' do @@ -76,5 +82,11 @@ describe 'Projects > Settings > Visibility settings', :js do expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled' expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end + + context 'disable email notifications' do + it 'is not available' do + expect(page).not_to have_selector('.js-emails-disabled', visible: true) + end + end end end diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb index 5e9c98428cf..851a09cf28a 100644 --- a/spec/features/projects/show/user_manages_notifications_spec.rb +++ b/spec/features/projects/show/user_manages_notifications_spec.rb @@ -65,4 +65,12 @@ describe 'Projects > Show > User manages notifications', :js do end end end + + context 'when project emails are disabled' do + let(:project) { create(:project, :public, :repository, emails_disabled: true) } + + it 'is disabled' do + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 1d8c9e7e426..8e4db2ca840 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -781,4 +781,39 @@ describe 'Login' do end end end + + context 'when sending confirmation email and not yet confirmed' do + let!(:user) { create(:user, confirmed_at: nil) } + let(:grace_period) { 2.days } + + before do + stub_application_setting(send_user_confirmation_email: true) + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period + end + + it 'allows login and shows a flash warning to confirm the email address' do + expect(authentication_metrics).to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(current_path).to eq root_path + expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address.") + end + + context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do + it 'does not allow login and shows a flash alert to confirm the email address' do + travel_to((grace_period + 1.day).from_now) do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + .and increment(:user_session_destroyed_counter).twice + + gitlab_sign_in(user) + + expect(current_path).to eq new_user_session_path + expect(page).to have_content('You have to confirm your email address before continuing.') + end + end + end + end end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index cf57fafc4f5..fb927a9ca3b 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -166,24 +166,51 @@ describe 'Signup' do end context 'with no errors' do - context "when sending confirmation email" do + context 'when sending confirmation email' do before do stub_application_setting(send_user_confirmation_email: true) end - it 'creates the user account and sends a confirmation email' do - visit root_path + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + end - fill_in 'new_user_name', with: new_user.name - fill_in 'new_user_username', with: new_user.username - fill_in 'new_user_email', with: new_user.email - fill_in 'new_user_email_confirmation', with: new_user.email - fill_in 'new_user_password', with: new_user.password + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + + expect { click_button 'Register' }.to change { User.count }.by(1) + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content('Please check your email to confirm your account') + end + end + + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + end + + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password - expect { click_button 'Register' }.to change { User.count }.by(1) + expect { click_button 'Register' }.to change { User.count }.by(1) - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.") + end end end diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 0828f113495..9216ad0060b 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -3,6 +3,7 @@ "required": [ "sha", "created_at", + "finished_at", "iid", "tag", "last?", @@ -11,6 +12,7 @@ ], "properties": { "created_at": { "type": "string" }, + "finished_at": { "type": ["string", "null"] }, "id": { "type": "integer" }, "iid": { "type": "integer" }, "last?": { "type": "boolean" }, diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js new file mode 100644 index 00000000000..806602877ef --- /dev/null +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -0,0 +1,123 @@ +import { shallowMount } from '@vue/test-utils'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const changedFile = () => ({ changed: true }); +const stagedFile = () => ({ changed: false, staged: true }); +const changedAndStagedFile = () => ({ changed: true, staged: true }); +const newFile = () => ({ changed: true, tempFile: true }); +const unchangedFile = () => ({ changed: false, tempFile: false, staged: false, deleted: false }); + +describe('Changed file icon', () => { + let wrapper; + + const factory = (props = {}) => { + wrapper = shallowMount(ChangedFileIcon, { + propsData: { + file: changedFile(), + showTooltip: true, + ...props, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findIcon = () => wrapper.find(Icon); + const findIconName = () => findIcon().props('name'); + const findIconClasses = () => + findIcon() + .props('cssClasses') + .split(' '); + const findTooltipText = () => wrapper.attributes('data-original-title'); + + it('with isCentered true, adds center class', () => { + factory({ + isCentered: true, + }); + + expect(wrapper.classes('ml-auto')).toBe(true); + }); + + it('with isCentered false, does not center', () => { + factory({ + isCentered: false, + }); + + expect(wrapper.classes('ml-auto')).toBe(false); + }); + + it('with showTooltip false, does not show tooltip', () => { + factory({ + showTooltip: false, + }); + + expect(findTooltipText()).toBeFalsy(); + }); + + describe.each` + file | iconName | tooltipText | desc + ${changedFile()} | ${'file-modified'} | ${'Unstaged modification'} | ${'with file changed'} + ${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'} + ${changedAndStagedFile()} | ${'file-modified'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'} + ${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'} + `('$desc', ({ file, iconName, tooltipText }) => { + beforeEach(() => { + factory({ file }); + }); + + it('renders icon', () => { + expect(findIconName()).toBe(iconName); + expect(findIconClasses()).toContain(iconName); + }); + + it('renders tooltip text', () => { + expect(findTooltipText()).toBe(tooltipText); + }); + }); + + describe('with file unchanged', () => { + beforeEach(() => { + factory({ + file: unchangedFile(), + }); + }); + + it('does not show icon', () => { + expect(findIcon().exists()).toBe(false); + }); + + it('does not have tooltip text', () => { + expect(findTooltipText()).toBe(''); + }); + }); + + it('with size set, sets icon size', () => { + const size = 8; + + factory({ + file: changedFile(), + size, + }); + + expect(findIcon().props('size')).toBe(size); + }); + + // NOTE: It looks like 'showStagedIcon' behavior is backwards to what the name suggests + // https://gitlab.com/gitlab-org/gitlab-ce/issues/66071 + it.each` + showStagedIcon | iconName | desc + ${false} | ${'file-modified-solid'} | ${'with showStagedIcon false, renders staged icon'} + ${true} | ${'file-modified'} | ${'with showStagedIcon true, renders regular icon'} + `('$desc', ({ showStagedIcon, iconName }) => { + factory({ + file: stagedFile(), + showStagedIcon, + }); + + expect(findIconName()).toEqual(iconName); + }); +}); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 037b16c90ed..98719697cea 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -262,4 +262,44 @@ describe GroupsHelper do expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json) end end + + describe '#can_disable_group_emails?' do + let(:current_user) { create(:user) } + let(:group) { create(:group, name: 'group') } + let(:subgroup) { create(:group, name: 'subgroup', parent: group) } + + before do + allow(helper).to receive(:current_user) { current_user } + end + + it 'returns true for the group owner' do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { true } + + expect(helper.can_disable_group_emails?(group)).to be_truthy + end + + it 'returns false for anyone else' do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { false } + + expect(helper.can_disable_group_emails?(group)).to be_falsey + end + + context 'when subgroups' do + before do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, subgroup) { true } + end + + it 'returns false if parent group is disabling emails' do + allow(group).to receive(:emails_disabled?).and_return(true) + + expect(helper.can_disable_group_emails?(subgroup)).to be_falsey + end + + it 'returns true if parent group is not disabling emails' do + allow(group).to receive(:emails_disabled?).and_return(false) + + expect(helper.can_disable_group_emails?(subgroup)).to be_truthy + end + end + end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 9ecaabc04ed..5717b15d656 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe NotificationsHelper do describe 'notification_icon' do it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') } + it { expect(notification_icon(:owner_disabled)).to match('class="fa fa-microphone-slash fa-fw"') } it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') } it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') } it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') } @@ -19,4 +20,14 @@ describe NotificationsHelper do it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') } it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') } end + + describe '#notification_icon_level' do + let(:user) { create(:user) } + let(:global_setting) { user.global_notification_setting } + let(:notification_setting) { create(:notification_setting, level: :watch) } + + it { expect(notification_icon_level(notification_setting, true)).to eq 'owner_disabled' } + it { expect(notification_icon_level(notification_setting)).to eq 'watch' } + it { expect(notification_icon_level(global_setting)).to eq 'participating' } + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 3716879c458..a70bfc2adc7 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -107,6 +107,30 @@ describe ProjectsHelper do end end + describe '#can_disable_emails?' do + let(:project) { create(:project) } + let(:user) { create(:project_member, :maintainer, user: create(:user), project: project).user } + + it 'returns true for the project owner' do + allow(helper).to receive(:can?).with(project.owner, :set_emails_disabled, project) { true } + + expect(helper.can_disable_emails?(project, project.owner)).to be_truthy + end + + it 'returns false for anyone else' do + allow(helper).to receive(:can?).with(user, :set_emails_disabled, project) { false } + + expect(helper.can_disable_emails?(project, user)).to be_falsey + end + + it 'returns false if group emails disabled' do + project = create(:project, group: create(:group)) + allow(project.group).to receive(:emails_disabled?).and_return(true) + + expect(helper.can_disable_emails?(project, project.owner)).to be_falsey + end + end + describe "readme_cache_key" do let(:project) { create(:project, :repository) } @@ -477,6 +501,7 @@ describe ProjectsHelper do it 'returns the command to push to create project over SSH' do allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' } + allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return('git@localhost:') expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') end diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index b78896c45fc..624d8b14c8f 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlToast } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; @@ -13,6 +15,7 @@ import MonitoringMock, { dashboardGitResponse, } from './mock_data'; +const localVue = createLocalVue(); const propsData = { hasMetrics: false, documentationPath: '/path/to/docs', @@ -59,7 +62,9 @@ describe('Dashboard', () => { }); afterEach(() => { - component.$destroy(); + if (component) { + component.$destroy(); + } mock.restore(); }); @@ -373,6 +378,51 @@ describe('Dashboard', () => { }); }); + describe('link to chart', () => { + let wrapper; + const currentDashboard = 'TEST_DASHBOARD'; + localVue.use(GlToast); + const link = () => wrapper.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + beforeEach(done => { + mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); + + wrapper = shallowMount(DashboardComponent, { + localVue, + sync: false, + attachToDocument: true, + propsData: { ...propsData, hasMetrics: true, currentDashboard }, + store, + }); + + setTimeout(done); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('adds a copy button to the dropdown', () => { + expect(link().text()).toContain('Generate link to chart'); + }); + + it('contains a link to the dashboard', () => { + expect(clipboardText()).toContain(`dashboard=${currentDashboard}`); + expect(clipboardText()).toContain(`group=`); + expect(clipboardText()).toContain(`title=`); + expect(clipboardText()).toContain(`y_label=`); + }); + + it('creates a toast when clicked', () => { + spyOn(wrapper.vm.$toast, 'show').and.stub(); + + link().vm.$emit('click'); + + expect(wrapper.vm.$toast.show).toHaveBeenCalled(); + }); + }); + describe('when the window resizes', () => { beforeEach(() => { mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js index 8ce24041e97..086be628093 100644 --- a/spec/javascripts/monitoring/panel_type_spec.js +++ b/spec/javascripts/monitoring/panel_type_spec.js @@ -1,20 +1,25 @@ import { shallowMount } from '@vue/test-utils'; import PanelType from '~/monitoring/components/panel_type.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; +import AreaChart from '~/monitoring/components/charts/area.vue'; import { graphDataPrometheusQueryRange } from './mock_data'; +import { createStore } from '~/monitoring/stores'; describe('Panel Type component', () => { + let store; let panelType; const dashboardWidth = 100; describe('When no graphData is available', () => { let glEmptyChart; - const graphDataNoResult = graphDataPrometheusQueryRange; + // Deep clone object before modifying + const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); graphDataNoResult.queries[0].result = []; beforeEach(() => { panelType = shallowMount(PanelType, { propsData: { + clipboardText: 'dashboard_link', dashboardWidth, graphData: graphDataNoResult, }, @@ -41,4 +46,33 @@ describe('Panel Type component', () => { }); }); }); + + describe('when Graph data is available', () => { + const exampleText = 'example_text'; + + beforeEach(() => { + store = createStore(); + panelType = shallowMount(PanelType, { + propsData: { + clipboardText: exampleText, + dashboardWidth, + graphData: graphDataPrometheusQueryRange, + }, + store, + }); + }); + + describe('Area Chart panel type', () => { + it('is rendered', () => { + expect(panelType.find(AreaChart).exists()).toBe(true); + }); + + it('sets clipboard text on the dropdown', () => { + const link = () => panelType.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + expect(clipboardText()).toBe(exampleText); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js deleted file mode 100644 index 634ba8403d5..00000000000 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import changedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('Changed file icon', () => { - let vm; - - function factory(props = {}) { - const component = Vue.extend(changedFileIcon); - - vm = createComponent(component, { - ...props, - file: { - tempFile: false, - changed: true, - }, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('centers icon', () => { - factory({ - isCentered: true, - }); - - expect(vm.$el.classList).toContain('ml-auto'); - }); - - describe('changedIcon', () => { - it('equals file-modified when not a temp file and has changes', () => { - factory(); - - expect(vm.changedIcon).toBe('file-modified'); - }); - - it('equals file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIcon).toBe('file-addition'); - }); - }); - - describe('changedIconClass', () => { - it('includes file-modified when not a temp file', () => { - factory(); - - expect(vm.changedIconClass).toContain('file-modified'); - }); - - it('includes file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIconClass).toContain('file-addition'); - }); - }); -}); diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb new file mode 100644 index 00000000000..7d67dc0251d --- /dev/null +++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb @@ -0,0 +1,296 @@ +# frozen_string_literal: true +require 'spec_helper' + +# rubocop: disable RSpec/FactoriesInMigrationSpecs +describe Gitlab::BackgroundMigration::LegacyUploadMover do + let(:test_dir) { FileUploader.options['storage_path'] } + let(:filename) { 'image.png' } + + let!(:namespace) { create(:namespace) } + let!(:legacy_project) { create(:project, :legacy_storage, namespace: namespace) } + let!(:hashed_project) { create(:project, namespace: namespace) } + # default project + let(:project) { legacy_project } + + let!(:issue) { create(:issue, project: project) } + let!(:note) { create(:note, note: 'some note', project: project, noteable: issue) } + + let(:legacy_upload) { create_upload(note, filename) } + + def create_remote_upload(model, filename) + create(:upload, :attachment_upload, + path: "note/attachment/#{model.id}/#{filename}", secret: nil, + store: ObjectStorage::Store::REMOTE, model: model) + end + + def create_upload(model, filename, with_file = true) + params = { + path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", + model: model, + store: ObjectStorage::Store::LOCAL + } + + if with_file + upload = create(:upload, :with_file, :attachment_upload, params) + model.update(attachment: upload.build_uploader) + model.attachment.upload + else + create(:upload, :attachment_upload, params) + end + end + + def new_upload + Upload.find_by(model_id: project.id, model_type: 'Project') + end + + def expect_error_log + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger| + expect(logger).to receive(:warn) + end + end + + shared_examples 'legacy upload deletion' do + it 'removes the upload record' do + described_class.new(legacy_upload).execute + + expect { legacy_upload.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + shared_examples 'move error' do + it 'does not remove the upload file' do + expect_error_log + + described_class.new(legacy_upload).execute + + expect(legacy_upload.reload).to eq(legacy_upload) + end + end + + shared_examples 'migrates the file correctly' do + before do + described_class.new(legacy_upload).execute + end + + it 'creates a new uplaod record correctly' do + expect(new_upload.secret).not_to be_nil + expect(new_upload.path).to end_with("#{new_upload.secret}/image.png") + expect(new_upload.model_id).to eq(project.id) + expect(new_upload.model_type).to eq('Project') + expect(new_upload.uploader).to eq('FileUploader') + end + + it 'updates the legacy upload note so that it references the file in the markdown' do + expected_path = File.join('/uploads', new_upload.secret, 'image.png') + expected_markdown = "some note \n ![image](#{expected_path})" + expect(note.reload.note).to eq(expected_markdown) + end + + it 'removes the attachment from the note model' do + expect(note.reload.attachment.file).to be_nil + end + end + + context 'when no model found for the upload' do + before do + legacy_upload.model = nil + expect_error_log + end + + it_behaves_like 'legacy upload deletion' + end + + context 'when the upload move fails' do + before do + expect(FileUploader).to receive(:copy_to).and_raise('failed') + end + + it_behaves_like 'move error' + end + + context 'when the upload is in local storage' do + shared_examples 'legacy local file' do + it 'removes the file correctly' do + expect(File.exist?(legacy_upload.absolute_path)).to be_truthy + + described_class.new(legacy_upload).execute + + expect(File.exist?(legacy_upload.absolute_path)).to be_falsey + end + + it 'moves legacy uploads to the correct location' do + described_class.new(legacy_upload).execute + + expected_path = File.join(test_dir, 'uploads', project.disk_path, new_upload.secret, filename) + expect(File.exist?(expected_path)).to be_truthy + end + end + + context 'when the upload file does not exist on the filesystem' do + let(:legacy_upload) { create_upload(note, filename, false) } + + before do + expect_error_log + end + + it_behaves_like 'legacy upload deletion' + end + + context 'when an upload belongs to a legacy_diff_note' do + let!(:merge_request) { create(:merge_request, source_project: project) } + let!(:note) do + create(:legacy_diff_note_on_merge_request, + note: 'some note', project: project, noteable: merge_request) + end + let(:legacy_upload) do + create(:upload, :with_file, :attachment_upload, + path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) + end + + context 'when the file does not exist for the upload' do + let(:legacy_upload) do + create(:upload, :attachment_upload, + path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file does not exist on expected path' do + let(:legacy_upload) do + create(:upload, :attachment_upload, :with_file, + path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file path does not include system/note/attachment' do + let(:legacy_upload) do + create(:upload, :attachment_upload, :with_file, + path: "uploads/-/system#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file move raises an error' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) + end + + it_behaves_like 'move error' + end + + context 'when the file can be handled correctly' do + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + + context 'when object storage is disabled for FileUploader' do + context 'when the file belongs to a legacy project' do + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + + context 'when object storage is enabled for FileUploader' do + # The process of migrating to object storage is a manual one, + # so it would go against expectations to automatically migrate these files + # to object storage during this migration. + # After this migration, these files should be able to successfully migrate to object storage. + + before do + stub_uploads_object_storage(FileUploader) + end + + context 'when the file belongs to a legacy project' do + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + end + + context 'when legacy uploads are stored in object storage' do + let(:legacy_upload) { create_remote_upload(note, filename) } + let(:remote_file) do + { key: "#{legacy_upload.path}" } + end + let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) } + let(:bucket) { connection.directories.create(key: 'uploads') } + + before do + stub_uploads_object_storage(FileUploader) + end + + shared_examples 'legacy remote file' do + it 'removes the file correctly' do + # expect(bucket.files.get(remote_file[:key])).to be_nil + + described_class.new(legacy_upload).execute + + expect(bucket.files.get(remote_file[:key])).to be_nil + end + + it 'moves legacy uploads to the correct remote location' do + described_class.new(legacy_upload).execute + + connection = ::Fog::Storage.new(FileUploader.object_store_credentials) + expect(connection.get_object('uploads', new_upload.path)[:status]).to eq(200) + end + end + + context 'when the upload file does not exist on the filesystem' do + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a legacy project' do + before do + bucket.files.create(remote_file) + end + + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy remote file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + before do + bucket.files.create(remote_file) + end + + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy remote file' + it_behaves_like 'legacy upload deletion' + end + end +end +# rubocop: enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb new file mode 100644 index 00000000000..ed8cbfeb11f --- /dev/null +++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require 'spec_helper' + +# rubocop: disable RSpec/FactoriesInMigrationSpecs +describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do + let(:test_dir) { FileUploader.options['storage_path'] } + + let!(:hashed_project) { create(:project) } + let!(:legacy_project) { create(:project, :legacy_storage) } + let!(:issue) { create(:issue, project: hashed_project) } + let!(:issue_legacy) { create(:issue, project: legacy_project) } + + let!(:note1) { create(:note, project: hashed_project, noteable: issue) } + let!(:note2) { create(:note, project: hashed_project, noteable: issue) } + let!(:note_legacy) { create(:note, project: legacy_project, noteable: issue_legacy) } + + def create_upload(model, with_file = true) + filename = 'image.png' + params = { + path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", + model: model, + store: ObjectStorage::Store::LOCAL + } + + if with_file + upload = create(:upload, :with_file, :attachment_upload, params) + model.update(attachment: upload.build_uploader) + model.attachment.upload + else + create(:upload, :attachment_upload, params) + end + end + + let!(:legacy_upload) { create_upload(note1) } + let!(:legacy_upload_no_file) { create_upload(note2, false) } + let!(:legacy_upload_legacy_project) { create_upload(note_legacy) } + + let(:start_id) { 1 } + let(:end_id) { 10000 } + + subject { described_class.new.perform(start_id, end_id) } + + it 'removes all legacy files' do + expect(File.exist?(legacy_upload.absolute_path)).to be_truthy + expect(File.exist?(legacy_upload_no_file.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_legacy_project.absolute_path)).to be_truthy + + subject + + expect(File.exist?(legacy_upload.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_no_file.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_legacy_project.absolute_path)).to be_falsey + end + + it 'removes all AttachmentUploader records' do + expect { subject }.to change { Upload.where(uploader: 'AttachmentUploader').count }.from(3).to(0) + end + + it 'creates new uploads for successfully migrated records' do + expect { subject }.to change { Upload.where(uploader: 'FileUploader').count }.from(0).to(2) + end +end +# rubocop: enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 5d4dec5899a..1a9350d68bd 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -20,20 +20,36 @@ describe Gitlab::Ci::Pipeline::Seed::Build do describe '#bridge?' do subject { seed_build.bridge? } - context 'when job is a bridge' do + context 'when job is a downstream bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end it { is_expected.to be_truthy } + + context 'when trigger definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: '' } } + end + + it { is_expected.to be_falsey } + end end - context 'when trigger definition is empty' do + context 'when job is an upstream bridge' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { trigger: '' } } + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } end - it { is_expected.to be_falsey } + it { is_expected.to be_truthy } + + context 'when upstream definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } + end + + it { is_expected.to be_falsey } + end end context 'when job is not a bridge' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 4ffa1fc9fd8..d5567b4f166 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1153,7 +1153,10 @@ module Gitlab stage_idx: 1, name: "test1", options: { - script: ["test"] + script: ["test"], + # This does not make sense, there is a follow-up: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/65569 + bridge_needs: %w[build1 build2] }, needs_attributes: [ { name: "build1" }, diff --git a/spec/lib/gitlab/git_post_receive_spec.rb b/spec/lib/gitlab/git_post_receive_spec.rb index 4c20d945585..f0df3794e29 100644 --- a/spec/lib/gitlab/git_post_receive_spec.rb +++ b/spec/lib/gitlab/git_post_receive_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ::Gitlab::GitPostReceive do - let(:project) { create(:project) } + set(:project) { create(:project, :repository) } subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) } @@ -92,4 +92,47 @@ describe ::Gitlab::GitPostReceive do end end end + + describe '#includes_default_branch?' do + context 'with no default branch' do + let(:changes) do + <<~EOF + 654321 210987 refs/heads/test1 + 654322 210986 refs/tags/#{project.default_branch} + 654323 210985 refs/heads/test3 + EOF + end + + it 'returns false' do + expect(subject.includes_default_branch?).to be_falsey + end + end + + context 'with a project with no default branch' do + let(:changes) do + <<~EOF + 654321 210987 refs/heads/test1 + EOF + end + + it 'returns true' do + expect(project).to receive(:default_branch).and_return(nil) + expect(subject.includes_default_branch?).to be_truthy + end + end + + context 'with default branch' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/test2 + 654323 210985 refs/heads/#{project.default_branch} + EOF + end + + it 'returns true' do + expect(subject.includes_default_branch?).to be_truthy + end + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 588c68d1fb0..9bbd9394d57 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -154,11 +154,6 @@ describe Gitlab::UsageData do expect(expected_keys - count_data.keys).to be_empty end - it 'does not gather user preferences usage data when the feature is disabled' do - stub_feature_flags(group_overview_security_dashboard: false) - expect(subject[:counts].keys).not_to include(:user_preferences) - end - it 'gathers projects data correctly' do count_data = subject[:counts] diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index eb32198265b..a871f9b3fe6 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -23,7 +23,7 @@ describe Ci::Bridge do let(:status) { bridge.detailed_status(user) } it 'returns detailed status object' do - expect(status).to be_a Gitlab::Ci::Status::Success + expect(status).to be_a Gitlab::Ci::Status::Created end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 1fb83fbb088..78be4a8131a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1929,6 +1929,13 @@ describe Ci::Pipeline, :mailer do it { is_expected.to be_an(Array) } end + describe '.bridgeable_statuses' do + subject { described_class.bridgeable_statuses } + + it { is_expected.to be_an(Array) } + it { is_expected.not_to include('created', 'preparing', 'pending') } + end + describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index e68de2e73a8..419e1dc2459 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1815,22 +1815,36 @@ describe Repository do end describe '#after_create' do + it 'calls expire_status_cache' do + expect(repository).to receive(:expire_status_cache) + + repository.after_create + end + + it 'logs an event' do + expect(repository).to receive(:repository_event).with(:create_repository) + + repository.after_create + end + end + + describe '#expire_status_cache' do it 'flushes the exists cache' do expect(repository).to receive(:expire_exists_cache) - repository.after_create + repository.expire_status_cache end it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) - repository.after_create + repository.expire_status_cache end it 'flushes the emptiness caches' do expect(repository).to receive(:expire_emptiness_caches) - repository.after_create + repository.expire_status_cache end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e8e17228523..5e6ff40e8cf 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -126,6 +126,12 @@ describe API::Commits do end end + context "with empty ref_name parameter" do + let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" } + + it_behaves_like 'project commits' + end + context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 76ad2aee5c5..c0ea2b3c389 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -32,6 +32,10 @@ describe DeploymentEntity do expect(subject).to include(:created_at) end + it 'exposes finished_at' do + expect(subject).to include(:finished_at) + end + context 'when the pipeline has another manual action' do let(:other_build) { create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline) } let!(:other_deployment) { create(:deployment, deployable: other_build) } diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 8af51848b7b..2bf7dc32436 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Git::BranchHooksService do include RepoHelpers + include ProjectForksHelper let(:project) { create(:project, :repository) } let(:user) { project.creator } @@ -158,9 +159,13 @@ describe Git::BranchHooksService do let(:blank_sha) { Gitlab::Git::BLANK_SHA } def clears_cache(extended: []) - expect(ProjectCacheWorker) - .to receive(:perform_async) - .with(project.id, extended, %i[commit_count repository_size]) + expect(service).to receive(:invalidated_file_types).and_return(extended) + + if extended.present? + expect(ProjectCacheWorker) + .to receive(:perform_async) + .with(project.id, extended, [], false) + end service.execute end @@ -268,10 +273,10 @@ describe Git::BranchHooksService do end describe 'Processing commit messages' do - # Create 4 commits, 2 of which have references. Limiting to 2 commits, we - # expect to see one commit message processor enqueued. - let(:commit_ids) do - Array.new(4) do |i| + # Create 6 commits, 3 of which have references. Limiting to 4 commits, we + # expect to see two commit message processors enqueued. + let!(:commit_ids) do + Array.new(6) do |i| message = "Issue #{'#' if i.even?}#{i}" project.repository.update_file( user, 'README.md', '', message: message, branch_name: branch @@ -279,18 +284,18 @@ describe Git::BranchHooksService do end end - let(:oldrev) { commit_ids.first } + let(:oldrev) { project.commit(commit_ids.first).parent_id } let(:newrev) { commit_ids.last } before do - stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2) + stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 4) end context 'creating the default branch' do let(:oldrev) { Gitlab::Git::BLANK_SHA } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -298,7 +303,7 @@ describe Git::BranchHooksService do context 'updating the default branch' do it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -319,7 +324,7 @@ describe Git::BranchHooksService do let(:oldrev) { Gitlab::Git::BLANK_SHA } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -329,7 +334,7 @@ describe Git::BranchHooksService do let(:branch) { 'fix' } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -345,6 +350,55 @@ describe Git::BranchHooksService do service.execute end end + + context 'when the project is forked' do + let(:upstream_project) { project } + let(:forked_project) { fork_project(upstream_project, user, repository: true) } + + let!(:forked_service) do + described_class.new(forked_project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + context 'when commits already exists in the upstream project' do + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + forked_service.execute + end + end + + context 'when a commit does not exist in the upstream repo' do + # On top of the existing 6 commits, 3 of which have references, + # create 2 more, 1 of which has a reference. Limiting to 4 commits, we + # expect to see one commit message processor enqueued. + let!(:forked_commit_ids) do + Array.new(2) do |i| + message = "Issue #{'#' if i.even?}#{i}" + forked_project.repository.update_file( + user, 'README.md', '', message: message, branch_name: branch + ) + end + end + + let(:newrev) { forked_commit_ids.last } + + it 'processes the commit message' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + forked_service.execute + end + end + + context 'when the upstream project no longer exists' do + it 'processes the commit messages' do + upstream_project.destroy! + + expect(ProcessCommitWorker).to receive(:perform_async).twice + + forked_service.execute + end + end + end end describe 'New branch detection' do diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 3b69b81f12e..c8a0c22b0e8 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -37,6 +37,29 @@ describe PostReceive do end describe "#process_project_changes" do + context 'with an empty project' do + let(:empty_project) { create(:project, :empty_repo) } + let(:changes) { "123456 789012 refs/heads/tést1\n" } + + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner) + allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, Gitlab::GlRepository::PROJECT]) + end + + it 'expire the status cache' do + expect(empty_project.repository).to receive(:expire_status_cache) + + perform + end + + it 'schedules a cache update for commit count and size' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(empty_project.id, [], [:repository_size, :commit_count], true) + + perform + end + end + context 'empty changes' do it "does not call any PushService but runs after project hooks" do expect(Git::BranchPushService).not_to receive(:new) @@ -67,15 +90,22 @@ describe PostReceive do context "branches" do let(:changes) do <<~EOF - '123456 789012 refs/heads/tést1' - '123456 789012 refs/heads/tést2' + 123456 789012 refs/heads/tést1 + 123456 789012 refs/heads/tést2 EOF end it 'expires the branches cache' do expect(project.repository).to receive(:expire_branches_cache).once - described_class.new.perform(gl_repository, key_id, base64_changes) + perform + end + + it 'expires the status cache' do + expect(project).to receive(:empty_repo?).and_return(true) + expect(project.repository).to receive(:expire_status_cache) + + perform end it 'calls Git::BranchPushService' do @@ -87,6 +117,30 @@ describe PostReceive do perform end + + it 'schedules a cache update for repository size only' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size], true) + + perform + end + + context 'with a default branch' do + let(:changes) do + <<~EOF + 123456 789012 refs/heads/tést1 + 123456 789012 refs/heads/tést2 + 678912 123455 refs/heads/#{project.default_branch} + EOF + end + + it 'schedules a cache update for commit count and size' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size, :commit_count], true) + + perform + end + end end context "tags" do @@ -107,7 +161,7 @@ describe PostReceive do it 'does not expire branches cache' do expect(project.repository).not_to receive(:expire_branches_cache) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end it "only invalidates tags once" do @@ -115,7 +169,7 @@ describe PostReceive do expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original expect(project.repository).to receive(:expire_tags_cache).once.and_call_original - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end it "calls Git::TagPushService" do @@ -129,6 +183,13 @@ describe PostReceive do perform end + + it 'schedules a single ProjectCacheWorker update' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size], true) + + perform + end end context "merge-requests" do diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 47bac63511e..eb1d3c364ac 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' describe ProcessCommitWorker do - include ProjectForksHelper - let(:worker) { described_class.new } let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } @@ -35,44 +33,6 @@ describe ProcessCommitWorker do worker.perform(project.id, user.id, commit.to_hash) end - - context 'when the project is forked' do - context 'when commit already exists in the upstream project' do - it 'does not process the commit message' do - forked = fork_project(project, user, repository: true) - - expect(worker).not_to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - - context 'when the commit does not exist in the upstream project' do - it 'processes the commit message' do - empty_project = create(:project, :public) - forked = fork_project(empty_project, user, repository: true) - - TestEnv.copy_repo(forked, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - - expect(worker).to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - - context 'when the upstream project no longer exists' do - it 'processes the commit message' do - forked = fork_project(project, user, repository: true) - project.destroy! - - expect(worker).to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - end end describe '#process_commit_message' do diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index edc55920b8e..7f3c4881b89 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -49,6 +49,16 @@ describe ProjectCacheWorker do worker.perform(project.id, %w(readme)) end + context 'with statistics disabled' do + let(:statistics) { [] } + + it 'does not update the project statistics' do + expect(worker).not_to receive(:update_statistics) + + worker.perform(project.id, [], [], false) + end + end + context 'with statistics' do let(:statistics) { %w(repository_size) } |