Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									b1e352740b
								
							
						
					
					
						commit
						1d9f78b3a4
					
				|  | @ -0,0 +1,9 @@ | |||
| ## Scope | ||||
| 
 | ||||
| This issue is part of a bigger development effort described in detail by its epic. The scope of this issue is to ... | ||||
| 
 | ||||
| ## Actions | ||||
| 
 | ||||
| <!-- Likely in the form of checkboxed elements --> | ||||
| 
 | ||||
| - [ ] TODO | ||||
|  | @ -2459,17 +2459,6 @@ Gitlab/FeatureAvailableUsage: | |||
| # WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490 | ||||
| Style/RegexpLiteralMixedPreserve: | ||||
|   Exclude: | ||||
|     - 'ee/app/models/status_page/project_setting.rb' | ||||
|     - 'ee/app/presenters/vulnerability_presenter.rb' | ||||
|     - 'ee/lib/api/geo_nodes.rb' | ||||
|     - 'ee/lib/gitlab/vulnerabilities/standard_vulnerability.rb' | ||||
|     - 'lib/api/invitations.rb' | ||||
|     - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb' | ||||
|     - 'lib/gitlab/metrics/requests_rack_middleware.rb' | ||||
|     - 'lib/gitlab/metrics/subscribers/active_record.rb' | ||||
|     - 'lib/gitlab/regex.rb' | ||||
|     - 'lib/gitlab/utils.rb' | ||||
|     - 'lib/product_analytics/tracker.rb' | ||||
|     - 'qa/qa/page/project/settings/advanced.rb' | ||||
|     - 'qa/spec/service/docker_run/gitlab_runner_spec.rb' | ||||
|     - 'rubocop/cop/gitlab/duplicate_spec_location.rb' | ||||
|  |  | |||
|  | @ -506,7 +506,10 @@ export default { | |||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       if (window.gon?.features?.diffsVirtualScrolling) { | ||||
|       if ( | ||||
|         window.gon?.features?.diffsVirtualScrolling || | ||||
|         window.gon?.features?.diffSearchingUsageData | ||||
|       ) { | ||||
|         let keydownTime; | ||||
|         Mousetrap.bind(['mod+f', 'mod+g'], () => { | ||||
|           keydownTime = new Date().getTime(); | ||||
|  | @ -520,6 +523,11 @@ export default { | |||
|             // and max 1000ms to be sure it the search box is filtered | ||||
|             if (delta >= 0 && delta < 1000) { | ||||
|               this.disableVirtualScroller = true; | ||||
| 
 | ||||
|               if (window.gon?.features?.diffSearchingUsageData) { | ||||
|                 api.trackRedisHllUserEvent('i_code_review_user_searches_diff'); | ||||
|                 api.trackRedisCounterEvent('user_searches_diffs'); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| import { mount2faAuthentication } from '~/authentication/mount_2fa'; | ||||
| 
 | ||||
| document.addEventListener('DOMContentLoaded', mount2faAuthentication); | ||||
| mount2faAuthentication(); | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle'; | ||||
| 
 | ||||
| document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle); | ||||
| initEnvironmentsFolderBundle(); | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| import monitoringApp from '~/monitoring/monitoring_app'; | ||||
| 
 | ||||
| document.addEventListener('DOMContentLoaded', monitoringApp); | ||||
| monitoringApp(); | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| import initTerminal from '~/terminal/'; | ||||
| 
 | ||||
| document.addEventListener('DOMContentLoaded', initTerminal); | ||||
| initTerminal(); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import $ from 'jquery'; | |||
| import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network'; | ||||
| import Network from '../network'; | ||||
| 
 | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
| (() => { | ||||
|   if (!$('.network-graph').length) return; | ||||
| 
 | ||||
|   const networkGraph = new Network({ | ||||
|  | @ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => { | |||
| 
 | ||||
|   // eslint-disable-next-line no-new
 | ||||
|   new ShortcutsNetwork(networkGraph.branch_graph); | ||||
| }); | ||||
| })(); | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui' | |||
| 
 | ||||
| import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin'; | ||||
| import { s__ } from '~/locale'; | ||||
| import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import { | ||||
|   visibilityOptions, | ||||
|   visibilityLevelDescriptions, | ||||
|  | @ -48,7 +47,7 @@ export default { | |||
|     GlFormCheckbox, | ||||
|     GlToggle, | ||||
|   }, | ||||
|   mixins: [settingsMixin, glFeatureFlagsMixin()], | ||||
|   mixins: [settingsMixin], | ||||
| 
 | ||||
|   props: { | ||||
|     requestCveAvailable: { | ||||
|  | @ -737,22 +736,5 @@ export default { | |||
|         }}</template> | ||||
|       </gl-form-checkbox> | ||||
|     </project-setting-row> | ||||
|     <project-setting-row | ||||
|       v-if="glFeatures.allowEditingCommitMessages" | ||||
|       ref="allow-editing-commit-messages" | ||||
|       class="gl-mb-4" | ||||
|     > | ||||
|       <input | ||||
|         :value="allowEditingCommitMessages" | ||||
|         type="hidden" | ||||
|         name="project[project_setting_attributes][allow_editing_commit_messages]" | ||||
|       /> | ||||
|       <gl-form-checkbox v-model="allowEditingCommitMessages"> | ||||
|         {{ s__('ProjectSettings|Allow editing commit messages') }} | ||||
|         <template #help>{{ | ||||
|           s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.') | ||||
|         }}</template> | ||||
|       </gl-form-checkbox> | ||||
|     </project-setting-row> | ||||
|   </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,5 +1,3 @@ | |||
| import initStaticSiteEditor from '~/static_site_editor'; | ||||
| 
 | ||||
| window.addEventListener('DOMContentLoaded', () => { | ||||
|   initStaticSiteEditor(document.querySelector('#static-site-editor')); | ||||
| }); | ||||
| initStaticSiteEditor(document.querySelector('#static-site-editor')); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
| import { GlModal, GlSprintf } from '@gitlab/ui'; | ||||
| import { GlModal, GlSprintf, GlFormInput } from '@gitlab/ui'; | ||||
| import { n__ } from '~/locale'; | ||||
| import { | ||||
|   REMOVE_TAG_CONFIRMATION_TEXT, | ||||
|  | @ -12,6 +12,7 @@ export default { | |||
|   components: { | ||||
|     GlModal, | ||||
|     GlSprintf, | ||||
|     GlFormInput, | ||||
|   }, | ||||
|   props: { | ||||
|     itemsToBeDeleted: { | ||||
|  | @ -25,7 +26,15 @@ export default { | |||
|       required: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       projectPath: '', | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     imageProjectPath() { | ||||
|       return this.itemsToBeDeleted[0]?.project?.path; | ||||
|     }, | ||||
|     modalTitle() { | ||||
|       if (this.deleteImage) { | ||||
|         return DELETE_IMAGE_CONFIRMATION_TITLE; | ||||
|  | @ -40,6 +49,7 @@ export default { | |||
|       if (this.deleteImage) { | ||||
|         return { | ||||
|           message: DELETE_IMAGE_CONFIRMATION_TEXT, | ||||
|           item: this.imageProjectPath, | ||||
|         }; | ||||
|       } | ||||
|       if (this.itemsToBeDeleted.length > 1) { | ||||
|  | @ -55,6 +65,9 @@ export default { | |||
|         item: first?.path, | ||||
|       }; | ||||
|     }, | ||||
|     disablePrimaryButton() { | ||||
|       return this.deleteImage && this.projectPath !== this.imageProjectPath; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     show() { | ||||
|  | @ -69,10 +82,14 @@ export default { | |||
|     ref="deleteModal" | ||||
|     modal-id="delete-tag-modal" | ||||
|     ok-variant="danger" | ||||
|     :action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }" | ||||
|     :action-primary="{ | ||||
|       text: __('Delete'), | ||||
|       attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }], | ||||
|     }" | ||||
|     :action-cancel="{ text: __('Cancel') }" | ||||
|     @primary="$emit('confirmDelete')" | ||||
|     @cancel="$emit('cancelDelete')" | ||||
|     @change="projectPath = ''" | ||||
|   > | ||||
|     <template #modal-title>{{ modalTitle }}</template> | ||||
|     <p v-if="modalDescription" data-testid="description"> | ||||
|  | @ -80,7 +97,13 @@ export default { | |||
|         <template #item> | ||||
|           <b>{{ modalDescription.item }}</b> | ||||
|         </template> | ||||
|         <template #code> | ||||
|           <code>{{ modalDescription.item }}</code> | ||||
|         </template> | ||||
|       </gl-sprintf> | ||||
|     </p> | ||||
|     <div v-if="deleteImage"> | ||||
|       <gl-form-input v-model="projectPath" /> | ||||
|     </div> | ||||
|   </gl-modal> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script> | ||||
| import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; | ||||
| import { GlIcon, GlTooltipDirective, GlDropdown, GlDropdownItem } from '@gitlab/ui'; | ||||
| import { sprintf, n__, s__ } from '~/locale'; | ||||
| import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; | ||||
| import TitleArea from '~/vue_shared/components/registry/title_area.vue'; | ||||
|  | @ -27,7 +27,7 @@ import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_cont | |||
| 
 | ||||
| export default { | ||||
|   name: 'DetailsHeader', | ||||
|   components: { GlButton, GlIcon, TitleArea, MetadataItem }, | ||||
|   components: { GlIcon, TitleArea, MetadataItem, GlDropdown, GlDropdownItem }, | ||||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|  | @ -143,9 +143,22 @@ export default { | |||
|       /> | ||||
|     </template> | ||||
|     <template #right-actions> | ||||
|       <gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')"> | ||||
|       <gl-dropdown | ||||
|         icon="ellipsis_v" | ||||
|         text="More actions" | ||||
|         :text-sr-only="true" | ||||
|         category="tertiary" | ||||
|         no-caret | ||||
|         right | ||||
|       > | ||||
|         <gl-dropdown-item | ||||
|           variant="danger" | ||||
|           :disabled="deleteButtonDisabled" | ||||
|           @click="$emit('delete')" | ||||
|         > | ||||
|           {{ __('Delete image repository') }} | ||||
|       </gl-button> | ||||
|         </gl-dropdown-item> | ||||
|       </gl-dropdown> | ||||
|     </template> | ||||
|   </title-area> | ||||
| </template> | ||||
|  |  | |||
|  | @ -99,7 +99,7 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__( | |||
| 
 | ||||
| export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?'); | ||||
| export const DELETE_IMAGE_CONFIRMATION_TEXT = s__( | ||||
|   'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone.', | ||||
|   'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}', | ||||
| ); | ||||
| 
 | ||||
| export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__( | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ query getContainerRepositoryDetails($id: ID!) { | |||
|     expirationPolicyCleanupStatus | ||||
|     project { | ||||
|       visibility | ||||
|       path | ||||
|       containerExpirationPolicy { | ||||
|         enabled | ||||
|         nextRunAt | ||||
|  |  | |||
|  | @ -161,7 +161,7 @@ export default { | |||
|     }, | ||||
|     deleteImage() { | ||||
|       this.deleteImageAlert = true; | ||||
|       this.itemsToBeDeleted = [{ path: this.containerRepository.path }]; | ||||
|       this.itemsToBeDeleted = [{ ...this.containerRepository }]; | ||||
|       this.$refs.deleteModal.show(); | ||||
|     }, | ||||
|     deleteImageError() { | ||||
|  |  | |||
|  | @ -1,6 +1,3 @@ | |||
| import initSourcegraph from './index'; | ||||
| 
 | ||||
| /** | ||||
|  * Load sourcegraph in it's own listener so that it's isolated from failures. | ||||
|  */ | ||||
| document.addEventListener('DOMContentLoaded', initSourcegraph); | ||||
| initSourcegraph(); | ||||
|  |  | |||
|  | @ -0,0 +1,96 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Analytics | ||||
|   module CycleAnalytics | ||||
|     module StageActions | ||||
|       include Gitlab::Utils::StrongMemoize | ||||
|       extend ActiveSupport::Concern | ||||
| 
 | ||||
|       included do | ||||
|         include CycleAnalyticsParams | ||||
| 
 | ||||
|         before_action :validate_params, only: %i[median] | ||||
|       end | ||||
| 
 | ||||
|       def index | ||||
|         result = list_service.execute | ||||
| 
 | ||||
|         if result.success? | ||||
|           render json: cycle_analytics_configuration(result.payload[:stages]) | ||||
|         else | ||||
|           render json: { message: result.message }, status: result.http_status | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def median | ||||
|         render json: { value: data_collector.median.seconds } | ||||
|       end | ||||
| 
 | ||||
|       def average | ||||
|         render json: { value: data_collector.average.seconds } | ||||
|       end | ||||
| 
 | ||||
|       def records | ||||
|         serialized_records = data_collector.serialized_records do |relation| | ||||
|           add_pagination_headers(relation) | ||||
|         end | ||||
| 
 | ||||
|         render json: serialized_records | ||||
|       end | ||||
| 
 | ||||
|       def count | ||||
|         render json: { count: data_collector.count } | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def parent | ||||
|         raise NotImplementedError | ||||
|       end | ||||
| 
 | ||||
|       def value_stream_class | ||||
|         raise NotImplementedError | ||||
|       end | ||||
| 
 | ||||
|       def add_pagination_headers(relation) | ||||
|         Gitlab::Pagination::OffsetHeaderBuilder.new( | ||||
|           request_context: self, | ||||
|           per_page: relation.limit_value, | ||||
|           page: relation.current_page, | ||||
|           next_page: relation.next_page, | ||||
|           prev_page: relation.prev_page, | ||||
|           params: permitted_cycle_analytics_params | ||||
|         ).execute(exclude_total_headers: true, data_without_counts: true) | ||||
|       end | ||||
| 
 | ||||
|       def stage | ||||
|         @stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute | ||||
|       end | ||||
| 
 | ||||
|       def data_collector | ||||
|         @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new( | ||||
|           stage: stage, | ||||
|           params: request_params.to_data_collector_params | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       def value_stream | ||||
|         @value_stream ||= value_stream_class.build_default_value_stream(parent) | ||||
|       end | ||||
| 
 | ||||
|       def list_params | ||||
|         { value_stream: value_stream } | ||||
|       end | ||||
| 
 | ||||
|       def list_service | ||||
|         Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params) | ||||
|       end | ||||
| 
 | ||||
|       def cycle_analytics_configuration(stages) | ||||
|         stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) } | ||||
| 
 | ||||
|         Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -16,8 +16,19 @@ module CycleAnalyticsParams | |||
|   end | ||||
| 
 | ||||
|   def options(params) | ||||
|     @options ||= { from: start_date(params), current_user: current_user }.merge(date_range(params)) | ||||
|     @options ||= {}.tap do |opts| | ||||
|       opts[:current_user] = current_user | ||||
|       opts[:projects] = params[:project_ids] if params[:project_ids] | ||||
|       opts[:group] = params[:group_id] if params[:group_id] | ||||
|       opts[:from] = params[:from] || start_date(params) | ||||
|       opts[:to] = params[:to] if params[:to] | ||||
|       opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter] | ||||
|       opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES)) | ||||
|       opts.merge!(date_range(params)) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def start_date(params) | ||||
|     case params[:start_date] | ||||
|  | @ -41,6 +52,27 @@ module CycleAnalyticsParams | |||
|     date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field) | ||||
|     date.to_time.utc | ||||
|   end | ||||
| 
 | ||||
|   def permitted_cycle_analytics_params | ||||
|     params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION) | ||||
|   end | ||||
| 
 | ||||
|   def all_cycle_analytics_params | ||||
|     permitted_cycle_analytics_params.merge(current_user: current_user) | ||||
|   end | ||||
| 
 | ||||
|   def request_params | ||||
|     @request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params) | ||||
|   end | ||||
| 
 | ||||
|   def validate_params | ||||
|     if request_params.invalid? | ||||
|       render( | ||||
|         json: { message: 'Invalid parameters', errors: request_params.errors }, | ||||
|         status: :unprocessable_entity | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams') | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController | |||
|       project_pipelines_url(group.projects.first) | ||||
|     when :trial | ||||
|       'https://about.gitlab.com/free-trial/' | ||||
|     when :team | ||||
|     when :team, :team_short | ||||
|       group_group_members_url(group) | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController | ||||
|   include ::Analytics::CycleAnalytics::StageActions | ||||
|   extend ::Gitlab::Utils::Override | ||||
| 
 | ||||
|   respond_to :json | ||||
| 
 | ||||
|   feature_category :planning_analytics | ||||
|  | @ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat | |||
|   before_action :authorize_read_cycle_analytics! | ||||
|   before_action :only_default_value_stream_is_allowed! | ||||
| 
 | ||||
|   def index | ||||
|     result = list_service.execute | ||||
| 
 | ||||
|     if result.success? | ||||
|       render json: cycle_analytics_configuration(result.payload[:stages]) | ||||
|     else | ||||
|       render json: { message: result.message }, status: result.http_status | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   override :parent | ||||
|   def parent | ||||
|     @project | ||||
|   end | ||||
| 
 | ||||
|   override :value_stream_class | ||||
|   def value_stream_class | ||||
|     Analytics::CycleAnalytics::ProjectValueStream | ||||
|   end | ||||
| 
 | ||||
|   def only_default_value_stream_is_allowed! | ||||
|     render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME | ||||
|   end | ||||
| 
 | ||||
|   def value_stream | ||||
|     Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project) | ||||
|   end | ||||
| 
 | ||||
|   def list_params | ||||
|     { value_stream: value_stream } | ||||
|   end | ||||
| 
 | ||||
|   def list_service | ||||
|     Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params) | ||||
|   end | ||||
| 
 | ||||
|   def cycle_analytics_configuration(stages) | ||||
|     stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) } | ||||
| 
 | ||||
|     Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -57,8 +57,14 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic | |||
|   def diffs_metadata | ||||
|     diffs = @compare.diffs(diff_options) | ||||
| 
 | ||||
|     options = additional_attributes.merge( | ||||
|       only_context_commits: show_only_context_commits?, | ||||
|       merge_ref_head_diff: render_merge_ref_head_diff?, | ||||
|       allow_tree_conflicts: display_merge_conflicts_in_diff? | ||||
|     ) | ||||
| 
 | ||||
|     render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user) | ||||
|                    .represent(diffs, additional_attributes.merge(only_context_commits: show_only_context_commits?)) | ||||
|                    .represent(diffs, options) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo | |||
|     # Usage data feature flags | ||||
|     push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml) | ||||
|     push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml) | ||||
|     push_frontend_feature_flag(:diff_searching_usage_data, @project, default_enabled: :yaml) | ||||
| 
 | ||||
|     experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance| | ||||
|       experiment_instance.exclude! unless helpers.can_import_members? | ||||
|  |  | |||
|  | @ -31,10 +31,6 @@ class ProjectsController < Projects::ApplicationController | |||
|   # Project Export Rate Limit | ||||
|   before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export] | ||||
| 
 | ||||
|   before_action only: [:edit] do | ||||
|     push_frontend_feature_flag(:allow_editing_commit_messages, @project) | ||||
|   end | ||||
| 
 | ||||
|   before_action do | ||||
|     push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) | ||||
|     push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml) | ||||
|  | @ -399,7 +395,6 @@ class ProjectsController < Projects::ApplicationController | |||
|     %i[ | ||||
|       show_default_award_emojis | ||||
|       squash_option | ||||
|       allow_editing_commit_messages | ||||
|       mr_default_target_self | ||||
|     ] | ||||
|   end | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ class SearchController < ApplicationController | |||
|   include SearchHelper | ||||
|   include RedisTracking | ||||
| 
 | ||||
|   RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show].freeze | ||||
| 
 | ||||
|   track_redis_hll_event :show, name: 'i_search_total' | ||||
| 
 | ||||
|   around_action :allow_gitaly_ref_name_caching | ||||
|  | @ -154,13 +156,22 @@ class SearchController < ApplicationController | |||
|   end | ||||
| 
 | ||||
|   def render_timeout(exception) | ||||
|     raise exception unless action_name.to_sym == :show | ||||
|     raise exception unless action_name.to_sym.in?(RESCUE_FROM_TIMEOUT_ACTIONS) | ||||
| 
 | ||||
|     log_exception(exception) | ||||
| 
 | ||||
|     @timeout = true | ||||
| 
 | ||||
|     if count_action_name? | ||||
|       render json: {}, status: :request_timeout | ||||
|     else | ||||
|       render status: :request_timeout | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def count_action_name? | ||||
|     action_name.to_sym == :count | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| SearchController.prepend_mod_with('SearchController') | ||||
|  |  | |||
|  | @ -31,6 +31,12 @@ module Mutations | |||
|     def ready?(**args) | ||||
|       raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only? | ||||
| 
 | ||||
|       missing_args = self.class.arguments.values | ||||
|         .reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) } | ||||
|         .map(&:graphql_name) | ||||
| 
 | ||||
|       raise ArgumentError, "Arguments must be provided: #{missing_args.join(", ")}" if missing_args.any? | ||||
| 
 | ||||
|       true | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,17 +7,8 @@ module Mutations | |||
| 
 | ||||
|       argument :due_date, | ||||
|                Types::TimeType, | ||||
|                required: false, | ||||
|                description: 'The desired due date for the issue, ' \ | ||||
|                'due date will be removed if absent or set to null' | ||||
| 
 | ||||
|       def ready?(**args) | ||||
|         unless args.key?(:due_date) | ||||
|           raise Gitlab::Graphql::Errors::ArgumentError, 'Argument dueDate must be provided (`null` accepted)' | ||||
|         end | ||||
| 
 | ||||
|         super | ||||
|       end | ||||
|                required: :nullable, | ||||
|                description: 'The desired due date for the issue. Due date is removed if null.' | ||||
| 
 | ||||
|       def resolve(project_path:, iid:, due_date:) | ||||
|         issue = authorized_find!(project_path: project_path, iid: iid) | ||||
|  |  | |||
|  | @ -10,7 +10,29 @@ module Types | |||
|       @deprecation = gitlab_deprecation(kwargs) | ||||
|       @doc_reference = kwargs.delete(:see) | ||||
| 
 | ||||
|       # our custom addition `nullable` which allows us to declare | ||||
|       # an argument that must be provided, even if its value is null. | ||||
|       # When `required: true` then required arguments must not be null. | ||||
|       @gl_required = !!kwargs[:required] | ||||
|       @gl_nullable = kwargs[:required] == :nullable | ||||
| 
 | ||||
|       # Only valid if an argument is also required. | ||||
|       if @gl_nullable | ||||
|         # Since the framework asserts that "required" means "cannot be null" | ||||
|         # we have to switch off "required" but still do the check in `ready?` behind the scenes | ||||
|         kwargs[:required] = false | ||||
|       end | ||||
| 
 | ||||
|       super(*args, **kwargs, &block) | ||||
|     end | ||||
| 
 | ||||
|     def accepts?(value) | ||||
|       # if the argument is declared as required, it must be included | ||||
|       return false if @gl_required && value == :not_given | ||||
|       # if the argument is declared as required, the value can only be null IF it is also nullable. | ||||
|       return false if @gl_required && value.nil? && !@gl_nullable | ||||
| 
 | ||||
|       true | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -491,7 +491,6 @@ module ProjectsHelper | |||
| 
 | ||||
|   def project_permissions_settings(project) | ||||
|     feature = project.project_feature | ||||
| 
 | ||||
|     { | ||||
|       packagesEnabled: !!project.packages_enabled, | ||||
|       visibilityLevel: project.visibility_level, | ||||
|  | @ -511,7 +510,6 @@ module ProjectsHelper | |||
|       metricsDashboardAccessLevel: feature.metrics_dashboard_access_level, | ||||
|       operationsAccessLevel: feature.operations_access_level, | ||||
|       showDefaultAwardEmojis: project.show_default_award_emojis?, | ||||
|       allowEditingCommitMessages: project.allow_editing_commit_messages?, | ||||
|       securityAndComplianceAccessLevel: project.security_and_compliance_access_level | ||||
|     } | ||||
|   end | ||||
|  |  | |||
|  | @ -69,21 +69,26 @@ class WebHook < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   def disable! | ||||
|     update!(recent_failures: FAILURE_THRESHOLD + 1) | ||||
|     update_attribute(:recent_failures, FAILURE_THRESHOLD + 1) | ||||
|   end | ||||
| 
 | ||||
|   def enable! | ||||
|     return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0 | ||||
| 
 | ||||
|     update!(recent_failures: 0, disabled_until: nil, backoff_count: 0) | ||||
|     assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0) | ||||
|     save(validate: false) | ||||
|   end | ||||
| 
 | ||||
|   def backoff! | ||||
|     update!(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES)) | ||||
|     assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES)) | ||||
|     save(validate: false) | ||||
|   end | ||||
| 
 | ||||
|   def failed! | ||||
|     update!(recent_failures: recent_failures + 1) if recent_failures < MAX_FAILURES | ||||
|     return unless recent_failures < MAX_FAILURES | ||||
| 
 | ||||
|     assign_attributes(recent_failures: recent_failures + 1) | ||||
|     save(validate: false) | ||||
|   end | ||||
| 
 | ||||
|   # Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited. | ||||
|  |  | |||
|  | @ -435,7 +435,7 @@ class Project < ApplicationRecord | |||
|   delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true | ||||
|   delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true | ||||
|   delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, | ||||
|     :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?, | ||||
|     :allow_merge_on_skipped_pipeline=, :has_confluence?, | ||||
|     to: :project_setting | ||||
|   delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ProjectSetting < ApplicationRecord | ||||
|   include IgnorableColumns | ||||
| 
 | ||||
|   ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10' | ||||
| 
 | ||||
|   belongs_to :project, inverse_of: :project_setting | ||||
| 
 | ||||
|   enum squash_option: { | ||||
|  |  | |||
|  | @ -19,7 +19,8 @@ module Users | |||
|       verify: 1, | ||||
|       trial: 2, | ||||
|       team: 3, | ||||
|       experience: 4 | ||||
|       experience: 4, | ||||
|       team_short: 5 | ||||
|     }, _suffix: true | ||||
| 
 | ||||
|     scope :without_track_and_series, -> (track, series) do | ||||
|  |  | |||
|  | @ -0,0 +1,24 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module DiffFileConflictType | ||||
|   extend ActiveSupport::Concern | ||||
|   include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|   included do | ||||
|     expose :conflict_type do |diff_file, options| | ||||
|       conflict_file = conflict_file(options, diff_file) | ||||
| 
 | ||||
|       next unless conflict_file | ||||
| 
 | ||||
|       conflict_file.conflict_type(diff_file) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def conflict_file(options, diff_file) | ||||
|     strong_memoize(:conflict_file) do | ||||
|       options[:conflicts] && options[:conflicts][diff_file.new_path] | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,6 +1,7 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class DiffFileEntity < DiffFileBaseEntity | ||||
|   include DiffFileConflictType | ||||
|   include CommitsHelper | ||||
|   include IconsHelper | ||||
|   include Gitlab::Utils::StrongMemoize | ||||
|  | @ -88,10 +89,4 @@ class DiffFileEntity < DiffFileBaseEntity | |||
|     # If nothing is present, inline will be the default. | ||||
|     options.fetch(:diff_view, :inline).to_sym | ||||
|   end | ||||
| 
 | ||||
|   def conflict_file(options, diff_file) | ||||
|     strong_memoize(:conflict_file) do | ||||
|       options[:conflicts] && options[:conflicts][diff_file.new_path] | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class DiffFileMetadataEntity < Grape::Entity | ||||
|   include DiffFileConflictType | ||||
| 
 | ||||
|   expose :added_lines | ||||
|   expose :removed_lines | ||||
|   expose :new_path | ||||
|  |  | |||
|  | @ -2,8 +2,13 @@ | |||
| 
 | ||||
| class DiffsMetadataEntity < DiffsEntity | ||||
|   unexpose :diff_files | ||||
|   expose :diff_files, using: DiffFileMetadataEntity do |diffs, _| | ||||
|     diffs.raw_diff_files(sorted: true) | ||||
|   expose :diff_files do |diffs, options| | ||||
|     DiffFileMetadataEntity.represent( | ||||
|       diffs.raw_diff_files(sorted: true), | ||||
|       options.merge( | ||||
|         conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts]) | ||||
|       ) | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   expose :conflict_resolution_path do |_, options| | ||||
|  |  | |||
|  | @ -8,8 +8,13 @@ module Namespaces | |||
|         completed_actions: [:created], | ||||
|         incomplete_actions: [:git_write] | ||||
|       }, | ||||
|       team_short: { | ||||
|         interval_days: [1], | ||||
|         completed_actions: [:git_write], | ||||
|         incomplete_actions: [:user_added] | ||||
|       }, | ||||
|       verify: { | ||||
|         interval_days: [1, 5, 10], | ||||
|         interval_days: [2, 6, 11], | ||||
|         completed_actions: [:git_write], | ||||
|         incomplete_actions: [:pipeline_created] | ||||
|       }, | ||||
|  | @ -98,13 +103,11 @@ module Namespaces | |||
| 
 | ||||
|     def can_perform_action?(user, group) | ||||
|       case track | ||||
|       when :create | ||||
|         user.can?(:create_projects, group) | ||||
|       when :verify | ||||
|       when :create, :verify | ||||
|         user.can?(:create_projects, group) | ||||
|       when :trial | ||||
|         user.can?(:start_trial, group) | ||||
|       when :team | ||||
|       when :team, :team_short | ||||
|         user.can?(:admin_group_member, group) | ||||
|       when :experience | ||||
|         true | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| %p.profile-settings-content | ||||
|   = s_("DeployTokens|Pick a name for your unique deploy token.") | ||||
|   - group_deploy_tokens_help_link_url = help_page_path('user/project/deploy_tokens/index.md') | ||||
|   - group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url } | ||||
|   = s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe } | ||||
| 
 | ||||
| = form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f| | ||||
|   = form_errors(token) | ||||
|  | @ -7,23 +9,26 @@ | |||
|   .form-group | ||||
|     = f.label :name, class: 'label-bold' | ||||
|     = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true | ||||
|     .text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.') | ||||
| 
 | ||||
|   .form-group | ||||
|     = f.label :expires_at, _('Expires at (optional)'), class: 'label-bold' | ||||
|     = f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold' | ||||
|     = f.text_field :expires_at, class: 'datepicker form-control', data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at | ||||
|     .text-secondary= s_('DeployTokens|Unless you enter a date, the token does not expire.') | ||||
|     .text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.') | ||||
| 
 | ||||
|   .form-group | ||||
|     = f.label :username, _('Username (optional)'), class: 'label-bold' | ||||
|     = f.text_field :username, class: 'form-control' | ||||
|     .text-secondary= s_('DeployTokens|Unless you specify a username, it is set to "gitlab+deploy-token-{n}".') | ||||
|     .text-secondary | ||||
|       = html_escape(s_('DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe } | ||||
| 
 | ||||
|   .form-group | ||||
|     = f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold' | ||||
|     = f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold' | ||||
|     %fieldset.form-group.form-check | ||||
|       = f.check_box :read_repository, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_repository_checkbox' } | ||||
|       = f.label :read_repository, 'read_repository', class: 'label-bold form-check-label' | ||||
|       .text-secondary= s_('DeployTokens|Allows read-only access to the repository.') | ||||
|       .text-secondary | ||||
|         = s_('DeployTokens|Allows read-only access to the repository.') | ||||
| 
 | ||||
|     - if container_registry_enabled?(group_or_project) | ||||
|       %fieldset.form-group.form-check | ||||
|  | @ -34,18 +39,18 @@ | |||
|       %fieldset.form-group.form-check | ||||
|         = f.check_box :write_registry, class: 'form-check-input' | ||||
|         = f.label :write_registry, 'write_registry', class: 'label-bold form-check-label' | ||||
|         .text-secondary= s_('DeployTokens|Allows write access to registry images.') | ||||
|         .text-secondary= s_('DeployTokens|Allows read and write access to registry images.') | ||||
| 
 | ||||
|     - if packages_registry_enabled?(group_or_project) | ||||
|       %fieldset.form-group.form-check | ||||
|         = f.check_box :read_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } | ||||
|         = f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label' | ||||
|         .text-secondary= s_('DeployTokens|Allows read access to the package registry.') | ||||
|         .text-secondary= s_('DeployTokens|Allows read-only access to the package registry.') | ||||
| 
 | ||||
|       %fieldset.form-group.form-check | ||||
|         = f.check_box :write_package_registry, class: 'form-check-input' | ||||
|         = f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label' | ||||
|         .text-secondary= s_('DeployTokens|Allows write access to the package registry.') | ||||
|         .text-secondary= s_('DeployTokens|Allows read and write access to the package registry.') | ||||
| 
 | ||||
|   .gl-mt-3 | ||||
|     = f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' } | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
|     - if @new_deploy_token.persisted? | ||||
|       = render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token | ||||
|     %h5.gl-mt-0 | ||||
|       = s_('DeployTokens|Add a deploy token') | ||||
|       = s_('DeployTokens|New deploy token') | ||||
|     = render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens | ||||
|     %hr | ||||
|     = render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens | ||||
|  |  | |||
|  | @ -1,8 +0,0 @@ | |||
| --- | ||||
| name: allow_editing_commit_messages | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49152/ | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290779 | ||||
| milestone: '13.7' | ||||
| type: development | ||||
| group: | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,8 @@ | |||
| --- | ||||
| name: diff_searching_usage_data | ||||
| introduced_by_url: | ||||
| rollout_issue_url: | ||||
| milestone: '14.2' | ||||
| type: development | ||||
| group: group::code review | ||||
| default_enabled: true | ||||
|  | @ -65,6 +65,7 @@ | |||
|     - 'i_code_review_diff_multiple_files' | ||||
|     - 'i_code_review_user_load_conflict_ui' | ||||
|     - 'i_code_review_user_resolve_conflict' | ||||
|     - 'i_code_review_user_searches_diff' | ||||
| - name: code_review_category_monthly_active_users | ||||
|   operator: OR | ||||
|   source: redis | ||||
|  | @ -122,6 +123,7 @@ | |||
|     - 'i_code_review_diff_multiple_files' | ||||
|     - 'i_code_review_user_load_conflict_ui' | ||||
|     - 'i_code_review_user_resolve_conflict' | ||||
|     - 'i_code_review_user_searches_diff' | ||||
| - name: code_review_extension_category_monthly_active_users | ||||
|   operator: OR | ||||
|   source: redis | ||||
|  |  | |||
|  | @ -0,0 +1,21 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.code_review.i_code_review_user_searches_diff_monthly | ||||
| description: Count of users who search merge request diffs | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: implemented | ||||
| milestone: '14.2' | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522 | ||||
| time_frame: 28d | ||||
| data_source: redis_hll | ||||
| data_category: Optional | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,21 @@ | |||
| --- | ||||
| key_path: redis_hll_counters.code_review.i_code_review_user_searches_diff_weekly | ||||
| description: Count of users who search merge request diffs | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: implemented | ||||
| milestone: '14.2' | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522 | ||||
| time_frame: 7d | ||||
| data_source: redis_hll | ||||
| data_category: Optional | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,21 @@ | |||
| --- | ||||
| key_path: counts.diff_searches | ||||
| description: Total count of merge request diff searches | ||||
| product_section: dev | ||||
| product_stage: create | ||||
| product_group: group::code review | ||||
| product_category: code_review | ||||
| value_type: number | ||||
| status: implemented | ||||
| milestone: '14.2' | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522 | ||||
| time_frame: all | ||||
| data_source: redis | ||||
| data_category: Optional | ||||
| distribution: | ||||
|   - ce | ||||
|   - ee | ||||
| tier: | ||||
|   - free | ||||
|   - premium | ||||
|   - ultimate | ||||
|  | @ -0,0 +1,22 @@ | |||
| --- | ||||
| key_path: counts.in_product_marketing_email_team_short_0_cta_clicked | ||||
| name: "count_clicks_on_the_first_email_of_the_team_short_track_for_in_product_marketing_emails" | ||||
| description: Total clicks on the team_short track's first email | ||||
| product_section: | ||||
| product_stage: growth | ||||
| product_group: group::activation | ||||
| product_category: onboarding | ||||
| value_type: number | ||||
| status: implemented | ||||
| milestone: "14.2" | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854 | ||||
| time_frame: all | ||||
| data_source: database | ||||
| data_category: Optional | ||||
| distribution: | ||||
| - ce | ||||
| - ee | ||||
| tier: | ||||
| - free | ||||
| - premium | ||||
| - ultimate | ||||
|  | @ -0,0 +1,22 @@ | |||
| --- | ||||
| key_path: counts.in_product_marketing_email_team_short_0_sent | ||||
| name: "count_sent_first_email_of_the_team_short_track_for_in_product_marketing_emails" | ||||
| description: Total sent emails of the team_short track's first email | ||||
| product_section: | ||||
| product_stage: growth | ||||
| product_group: group::activation | ||||
| product_category: onboarding | ||||
| value_type: number | ||||
| status: implemented | ||||
| milestone: "14.2" | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854 | ||||
| time_frame: all | ||||
| data_source: database | ||||
| data_category: Optional | ||||
| distribution: | ||||
| - ce | ||||
| - ee | ||||
| tier: | ||||
| - free | ||||
| - premium | ||||
| - ultimate | ||||
|  | @ -283,7 +283,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do | |||
|           resource :cycle_analytics, only: :show, path: 'value_stream_analytics' | ||||
|           scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do | ||||
|             resources :value_streams, only: [:index] do | ||||
|               resources :stages, only: [:index] | ||||
|               resources :stages, only: [:index] do | ||||
|                 member do | ||||
|                   get :median | ||||
|                   get :average | ||||
|                   get :records | ||||
|                   get :count | ||||
|                 end | ||||
|               end | ||||
|             end | ||||
|             resource :summary, controller: :summary, only: :show | ||||
|           end | ||||
|  |  | |||
|  | @ -6,14 +6,10 @@ class AddAllowToEditCommitToProjectSettings < ActiveRecord::Migration[6.0] | |||
|   DOWNTIME = false | ||||
| 
 | ||||
|   def up | ||||
|     with_lock_retries do | ||||
|       add_column :project_settings, :allow_editing_commit_messages, :boolean, default: false, null: false | ||||
|     end | ||||
|     # no-op | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     with_lock_retries do | ||||
|       remove_column :project_settings, :allow_editing_commit_messages | ||||
|     end | ||||
|     # no-op | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -54,6 +54,16 @@ Returns [`CiConfig`](#ciconfig). | |||
| | <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. | | ||||
| | <a id="queryciconfigsha"></a>`sha` | [`String`](#string) | Sha for the pipeline. | | ||||
| 
 | ||||
| ### `Query.ciMinutesUsage` | ||||
| 
 | ||||
| The monthly CI minutes usage data for the current user. | ||||
| 
 | ||||
| Returns [`CiMinutesNamespaceMonthlyUsageConnection`](#ciminutesnamespacemonthlyusageconnection). | ||||
| 
 | ||||
| This field returns a [connection](#connections). It accepts the | ||||
| four standard [pagination arguments](#connection-pagination-arguments): | ||||
| `before: String`, `after: String`, `first: Int`, `last: Int`. | ||||
| 
 | ||||
| ### `Query.containerRepository` | ||||
| 
 | ||||
| Find a container repository. | ||||
|  | @ -2533,7 +2543,7 @@ Input type: `IssueSetDueDateInput` | |||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="mutationissuesetduedateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | ||||
| | <a id="mutationissuesetduedateduedate"></a>`dueDate` | [`Time`](#time) | The desired due date for the issue, due date will be removed if absent or set to null. | | ||||
| | <a id="mutationissuesetduedateduedate"></a>`dueDate` | [`Time`](#time) | The desired due date for the issue. Due date is removed if null. | | ||||
| | <a id="mutationissuesetduedateiid"></a>`iid` | [`String!`](#string) | The IID of the issue to mutate. | | ||||
| | <a id="mutationissuesetduedateprojectpath"></a>`projectPath` | [`ID!`](#id) | The project the issue to mutate is in. | | ||||
| 
 | ||||
|  | @ -4876,6 +4886,52 @@ The edge type for [`CiJob`](#cijob). | |||
| | <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | ||||
| | <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. | | ||||
| 
 | ||||
| #### `CiMinutesNamespaceMonthlyUsageConnection` | ||||
| 
 | ||||
| The connection type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage). | ||||
| 
 | ||||
| ##### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesnamespacemonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesNamespaceMonthlyUsageEdge]`](#ciminutesnamespacemonthlyusageedge) | A list of edges. | | ||||
| | <a id="ciminutesnamespacemonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesNamespaceMonthlyUsage]`](#ciminutesnamespacemonthlyusage) | A list of nodes. | | ||||
| | <a id="ciminutesnamespacemonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | ||||
| 
 | ||||
| #### `CiMinutesNamespaceMonthlyUsageEdge` | ||||
| 
 | ||||
| The edge type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage). | ||||
| 
 | ||||
| ##### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesnamespacemonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | ||||
| | <a id="ciminutesnamespacemonthlyusageedgenode"></a>`node` | [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage) | The item at the end of the edge. | | ||||
| 
 | ||||
| #### `CiMinutesProjectMonthlyUsageConnection` | ||||
| 
 | ||||
| The connection type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage). | ||||
| 
 | ||||
| ##### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesprojectmonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesProjectMonthlyUsageEdge]`](#ciminutesprojectmonthlyusageedge) | A list of edges. | | ||||
| | <a id="ciminutesprojectmonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesProjectMonthlyUsage]`](#ciminutesprojectmonthlyusage) | A list of nodes. | | ||||
| | <a id="ciminutesprojectmonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | ||||
| 
 | ||||
| #### `CiMinutesProjectMonthlyUsageEdge` | ||||
| 
 | ||||
| The edge type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage). | ||||
| 
 | ||||
| ##### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | ||||
| | <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. | | ||||
| 
 | ||||
| #### `CiRunnerConnection` | ||||
| 
 | ||||
| The connection type for [`CiRunner`](#cirunner). | ||||
|  | @ -7858,6 +7914,25 @@ Represents the total number of issues and their weights for a particular day. | |||
| | ---- | ---- | ----------- | | ||||
| | <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) | | ||||
| 
 | ||||
| ### `CiMinutesNamespaceMonthlyUsage` | ||||
| 
 | ||||
| #### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesnamespacemonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The total number of minutes used by all projects in the namespace. | | ||||
| | <a id="ciminutesnamespacemonthlyusagemonth"></a>`month` | [`String`](#string) | The month related to the usage data. | | ||||
| | <a id="ciminutesnamespacemonthlyusageprojects"></a>`projects` | [`CiMinutesProjectMonthlyUsageConnection`](#ciminutesprojectmonthlyusageconnection) | CI minutes usage data for projects in the namespace. (see [Connections](#connections)) | | ||||
| 
 | ||||
| ### `CiMinutesProjectMonthlyUsage` | ||||
| 
 | ||||
| #### Fields | ||||
| 
 | ||||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <a id="ciminutesprojectmonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The number of CI minutes used by the project in the month. | | ||||
| | <a id="ciminutesprojectmonthlyusagename"></a>`name` | [`String`](#string) | The name of the project. | | ||||
| 
 | ||||
| ### `CiRunner` | ||||
| 
 | ||||
| #### Fields | ||||
|  |  | |||
|  | @ -3277,7 +3277,7 @@ dashboards. | |||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0. | ||||
| > - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above. | ||||
| 
 | ||||
| The `terraform` report obtains a Terraform `tfplan.json` file. [JQ processing required to remove credentials](../../user/infrastructure/mr_integration.md#setup). The collected Terraform | ||||
| The `terraform` report obtains a Terraform `tfplan.json` file. [JQ processing required to remove credentials](../../user/infrastructure/mr_integration.md#configure-terraform-report-artifacts). The collected Terraform | ||||
| plan report uploads to GitLab as an artifact and displays | ||||
| in merge requests. For more information, see | ||||
| [Output `terraform plan` information into a merge request](../../user/infrastructure/mr_integration.md). | ||||
|  |  | |||
|  | @ -1366,6 +1366,31 @@ argument :my_arg, GraphQL::Types::String, | |||
|          description: "A description of the argument." | ||||
| ``` | ||||
| 
 | ||||
| #### Nullability | ||||
| 
 | ||||
| Arguments can be marked as `required: true` which means the value must be present and not `null`. | ||||
| If a required argument's value can be `null`, use the `required: :nullable` declaration. | ||||
| 
 | ||||
| Example: | ||||
| 
 | ||||
| ```ruby | ||||
| argument :due_date, | ||||
|          Types::TimeType, | ||||
|          required: :nullable, | ||||
|          description: 'The desired due date for the issue. Due date is removed if null.' | ||||
| ``` | ||||
| 
 | ||||
| In the above example, the `due_date` argument must be given, but unlike the GraphQL spec, the value can be `null`. | ||||
| This allows 'unsetting' the due date in a single mutation rather than creating a new mutation for removing the due date. | ||||
| 
 | ||||
| ```ruby | ||||
| { due_date: null } # => OK | ||||
| { due_date: "2025-01-10" } # => OK | ||||
| {  } # => invalid (not given) | ||||
| ``` | ||||
| 
 | ||||
| #### Keywords | ||||
| 
 | ||||
| Each GraphQL `argument` defined is passed to the `#resolve` method | ||||
| of a mutation as keyword arguments. | ||||
| 
 | ||||
|  | @ -1377,6 +1402,8 @@ def resolve(my_arg:) | |||
| end | ||||
| ``` | ||||
| 
 | ||||
| #### Input Types | ||||
| 
 | ||||
| `graphql-ruby` wraps up arguments into an | ||||
| [input type](https://graphql.org/learn/schema/#input-types). | ||||
| 
 | ||||
|  |  | |||
|  | @ -2638,6 +2638,34 @@ Status: `data_available` | |||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `counts.in_product_marketing_email_team_short_0_cta_clicked` | ||||
| 
 | ||||
| Total clicks on the team_short track's first email | ||||
| 
 | ||||
| [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml) | ||||
| 
 | ||||
| Group: `group::activation` | ||||
| 
 | ||||
| Data Category: `Optional` | ||||
| 
 | ||||
| Status: `implemented` | ||||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `counts.in_product_marketing_email_team_short_0_sent` | ||||
| 
 | ||||
| Total sent emails of the team_short track's first email | ||||
| 
 | ||||
| [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml) | ||||
| 
 | ||||
| Group: `group::activation` | ||||
| 
 | ||||
| Data Category: `Optional` | ||||
| 
 | ||||
| Status: `implemented` | ||||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `counts.in_product_marketing_email_trial_0_cta_clicked` | ||||
| 
 | ||||
| Total clicks on the verify trial's first email | ||||
|  | @ -7650,6 +7678,20 @@ Status: `data_available` | |||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `counts.user_searches_diffs` | ||||
| 
 | ||||
| Count of users who search merge request diffs | ||||
| 
 | ||||
| [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210723075525_user_searches_diffs.yml) | ||||
| 
 | ||||
| Group: `group::code review` | ||||
| 
 | ||||
| Data Category: `Optional` | ||||
| 
 | ||||
| Status: `implemented` | ||||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `counts.web_hooks` | ||||
| 
 | ||||
| Missing description | ||||
|  | @ -10646,6 +10688,20 @@ Status: `data_available` | |||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `redis_hll_counters.code_review.i_code_review_searches_in_diff_monthly` | ||||
| 
 | ||||
| Count of searches in merge request diffs | ||||
| 
 | ||||
| [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210722132444_i_code_review_searches_in_diff_monthly.yml) | ||||
| 
 | ||||
| Group: `group::code review` | ||||
| 
 | ||||
| Data Category: `Optional` | ||||
| 
 | ||||
| Status: `implemented` | ||||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `redis_hll_counters.code_review.i_code_review_user_add_suggestion_monthly` | ||||
| 
 | ||||
| Count of unique users per month who added a suggestion | ||||
|  | @ -11514,6 +11570,20 @@ Status: `data_available` | |||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `redis_hll_counters.code_review.i_code_review_user_searches_diff_monthly` | ||||
| 
 | ||||
| Count of users who search merge request diffs | ||||
| 
 | ||||
| [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210720144005_i_code_review_user_searches_diff_monthly.yml) | ||||
| 
 | ||||
| Group: `group::code review` | ||||
| 
 | ||||
| Data Category: `Optional` | ||||
| 
 | ||||
| Status: `implemented` | ||||
| 
 | ||||
| Tiers: `free`, `premium`, `ultimate` | ||||
| 
 | ||||
| ### `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly` | ||||
| 
 | ||||
| Count of unique users per month with diffs viewed file by file | ||||
|  |  | |||
|  | @ -0,0 +1,59 @@ | |||
| --- | ||||
| stage: Create | ||||
| group: Ecosystem | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||
| --- | ||||
| 
 | ||||
| # Configure the Jira integration in GitLab | ||||
| 
 | ||||
| You can set up the [Jira integration](index.md#jira-integration) | ||||
| by configuring your project settings in GitLab. | ||||
| You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration), | ||||
| and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration). | ||||
| 
 | ||||
| Prerequisites: | ||||
| 
 | ||||
| - Ensure your GitLab installation does not use a [relative URL](development_panel.md#limitations). | ||||
| - For **Jira Server**, ensure you have a Jira username and password. | ||||
|   See [authentication in Jira](index.md#authentication-in-jira). | ||||
| - For **Jira on Atlassian cloud**, ensure you have an API token | ||||
|   and the email address you used to create the token. | ||||
|   See [authentication in Jira](index.md#authentication-in-jira). | ||||
| 
 | ||||
| To configure your project: | ||||
| 
 | ||||
| 1. Go to your project and select [**Settings > Integrations**](../../user/project/integrations/overview.md#accessing-integrations). | ||||
| 1. Select **Jira**. | ||||
| 1. Select **Enable integration**. | ||||
| 1. Select **Trigger** actions. Your choice determines whether a mention of Jira issue | ||||
|    (in a GitLab commit, merge request, or both) creates a cross-link in Jira back to GitLab. | ||||
| 1. To comment in the Jira issue when a **Trigger** action is made in GitLab, select | ||||
|    **Enable comments**. | ||||
| 1. To transition Jira issues when a | ||||
|    [closing reference](../../user/project/issues/managing_issues.md#closing-issues-automatically) | ||||
|    is made in GitLab, select **Enable Jira transitions**. | ||||
| 1. Provide Jira configuration information: | ||||
|    - **Web URL**: The base URL to the Jira instance web interface you're linking to | ||||
|      this GitLab project, such as `https://jira.example.com`. | ||||
|    - **Jira API URL**: The base URL to the Jira instance API, such as `https://jira-api.example.com`. | ||||
|      Defaults to the **Web URL** value if not set. Leave blank if using **Jira on Atlassian cloud**. | ||||
|    - **Username or Email**: | ||||
|      For **Jira Server**, use `username`. For **Jira on Atlassian cloud**, use `email`. | ||||
|    - **Password/API token**: | ||||
|      Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. | ||||
| 1. To enable users to view Jira issues inside the GitLab project **(PREMIUM)**, select **Enable Jira issues** and | ||||
|    enter a Jira project key. | ||||
| 
 | ||||
|    You can display issues only from a single Jira project in a given GitLab project. | ||||
| 
 | ||||
|    WARNING: | ||||
|    If you enable Jira issues with this setting, all users with access to this GitLab project | ||||
|    can view all issues from the specified Jira project. | ||||
| 
 | ||||
| 1. To enable issue creation for vulnerabilities **(ULTIMATE)**, select **Enable Jira issues creation from vulnerabilities**. | ||||
| 1. Select the **Jira issue type**. If the dropdown is empty, select refresh (**{retry}**) and try again. | ||||
| 1. To verify the Jira connection is working, select **Test settings**. | ||||
| 1. Select **Save changes**. | ||||
| 
 | ||||
| Your GitLab project can now interact with all Jira projects in your instance and the project now | ||||
| displays a Jira link that opens the Jira project. | ||||
|  | @ -68,8 +68,8 @@ To simplify administration, we recommend that a GitLab group maintainer or group | |||
| 
 | ||||
| | Jira usage | GitLab.com customers need | GitLab self-managed customers need | | ||||
| |------------|---------------------------|------------------------------------| | ||||
| | [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances) for more information. | | ||||
| | Your own server | The Jira DVCS (distributed version control system) connector. This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). | | ||||
| | [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. For more information, see the documentation for the [GitLab.com for Jira Cloud app](connect-app.md). | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances) for more information. | | ||||
| | Your own server | The [Jira DVCS (distributed version control system) connector](dvcs.md). This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). | | ||||
| 
 | ||||
| Each GitLab project can be configured to connect to an entire Jira instance. That means after | ||||
| configuration, one GitLab project can interact with all Jira projects in that instance. For: | ||||
|  | @ -82,57 +82,6 @@ configuration, one GitLab project can interact with all Jira projects in that in | |||
| If you have a single Jira instance, you can pre-fill the settings. For more information, read the | ||||
| documentation for [central administration of project integrations](../../user/admin_area/settings/project_integration_management.md). | ||||
| 
 | ||||
| To enable the integration in GitLab, you must: | ||||
| 
 | ||||
| 1. [Configure the project in Jira](index.md#jira-integration). | ||||
|    The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`. | ||||
| 1. [Enter the correct values in GitLab](#configure-gitlab). | ||||
| 
 | ||||
| ### Configure GitLab | ||||
| 
 | ||||
| To enable the integration in your GitLab project, after you | ||||
| [configure your Jira project](index.md#jira-integration): | ||||
| 
 | ||||
| 1. Ensure your GitLab installation does not use a relative URL, as described in | ||||
|    [Limitations](#limitations). | ||||
| 1. Go to your project and select [**Settings > Integrations**](../../user/project/integrations/overview.md#accessing-integrations). | ||||
| 1. Select **Jira**. | ||||
| 1. Select **Enable integration**. | ||||
| 1. Select **Trigger** actions. Your choice determines whether a mention of Jira issue | ||||
|    (in a GitLab commit, merge request, or both) creates a cross-link in Jira back to GitLab. | ||||
| 1. To comment in the Jira issue when a **Trigger** action is made in GitLab, select | ||||
|    **Enable comments**. | ||||
| 1. To transition Jira issues when a | ||||
|    [closing reference](../../user/project/issues/managing_issues.md#closing-issues-automatically) | ||||
|    is made in GitLab, select **Enable Jira transitions**. | ||||
| 1. Provide Jira configuration information: | ||||
|    - **Web URL**: The base URL to the Jira instance web interface you're linking to | ||||
|      this GitLab project, such as `https://jira.example.com`. | ||||
|    - **Jira API URL**: The base URL to the Jira instance API, such as `https://jira-api.example.com`. | ||||
|      Defaults to the **Web URL** value if not set. Leave blank if using **Jira on Atlassian cloud**. | ||||
|    - **Username or Email**: | ||||
|      For **Jira Server**, use `username`. For **Jira on Atlassian cloud**, use `email`. | ||||
|      See [authentication in Jira](index.md#authentication-in-jira). | ||||
|    - **Password/API token**: | ||||
|      Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. | ||||
|      See [authentication in Jira](index.md#authentication-in-jira). | ||||
| 1. To enable users to view Jira issues inside the GitLab project **(PREMIUM)**, select **Enable Jira issues** and | ||||
|    enter a Jira project key. | ||||
| 
 | ||||
|    You can display issues only from a single Jira project in a given GitLab project. | ||||
| 
 | ||||
|    WARNING: | ||||
|    If you enable Jira issues with this setting, all users with access to this GitLab project | ||||
|    can view all issues from the specified Jira project. | ||||
| 
 | ||||
| 1. To enable issue creation for vulnerabilities **(ULTIMATE)**, select **Enable Jira issues creation from vulnerabilities**. | ||||
| 1. Select the **Jira issue type**. If the dropdown is empty, select refresh (**{retry}**) and try again. | ||||
| 1. To verify the Jira connection is working, select **Test settings**. | ||||
| 1. Select **Save changes**. | ||||
| 
 | ||||
| Your GitLab project can now interact with all Jira projects in your instance and the project now | ||||
| displays a Jira link that opens the Jira project. | ||||
| 
 | ||||
| ## Limitations | ||||
| 
 | ||||
| This integration is not supported on GitLab instances under a | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`. | |||
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> | ||||
| For an overview, see [Agile Management - GitLab-Jira Basic Integration](https://www.youtube.com/watch?v=fWvwkx5_00E&feature=youtu.be). | ||||
| 
 | ||||
| To set up the integration, [configure the project settings](development_panel.md#configure-gitlab) in GitLab. | ||||
| To set up the integration, [configure the project settings](configure.md) in GitLab. | ||||
| You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration), | ||||
| and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration). | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w | |||
| # Jira integration issue management **(FREE)** | ||||
| 
 | ||||
| Integrating issue management with Jira requires you to [configure Jira](index.md#jira-integration) | ||||
| and [enable the Jira service](development_panel.md#configure-gitlab) in GitLab. | ||||
| and [enable the Jira integration](configure.md) in GitLab. | ||||
| After you configure and enable the integration, you can reference and close Jira | ||||
| issues by mentioning the Jira ID in GitLab commits and merge requests. | ||||
| 
 | ||||
|  | @ -102,7 +102,7 @@ Consider this example: | |||
| > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. | ||||
| 
 | ||||
| You can browse, search, and view issues from a selected Jira project directly in GitLab, | ||||
| if your GitLab administrator [has configured it](development_panel.md#configure-gitlab). | ||||
| if your GitLab administrator [has configured it](configure.md). | ||||
| 
 | ||||
| To do this, in GitLab, go to your project and select **Jira > Issues list**. The issue list | ||||
| sorts by **Created date** by default, with the newest issues listed at the top: | ||||
|  | @ -149,7 +149,7 @@ When you configure automatic issue transitions, you can transition a referenced | |||
| Jira issue to the next available status with a category of **Done**. To configure | ||||
| this setting: | ||||
| 
 | ||||
| 1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions. | ||||
| 1. Refer to the [Configure GitLab](configure.md) instructions. | ||||
| 1. Select the **Enable Jira transitions** check box. | ||||
| 1. Select the **Move to Done** option. | ||||
| 
 | ||||
|  | @ -167,7 +167,7 @@ For advanced workflows, you can specify custom Jira transition IDs: | |||
|        **action** parameter in the URL. | ||||
|    The transition ID may vary between workflows (for example, a bug instead of a | ||||
|    story), even if the status you're changing to is the same. | ||||
| 1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions. | ||||
| 1. Refer to the [Configure GitLab](configure.md) instructions. | ||||
| 1. Select the **Enable Jira transitions** setting. | ||||
| 1. Select the **Custom transitions** option. | ||||
| 1. Enter your transition IDs in the text field. If you insert multiple transition IDs | ||||
|  | @ -179,7 +179,7 @@ For advanced workflows, you can specify custom Jira transition IDs: | |||
| GitLab can cross-link source commits or merge requests with Jira issues without | ||||
| adding a comment to the Jira issue: | ||||
| 
 | ||||
| 1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions. | ||||
| 1. Refer to the [Configure GitLab](configure.md) instructions. | ||||
| 1. Clear the **Enable comments** check box. | ||||
| 
 | ||||
| ## Enable or disable the ability to require an associated Jira issue on merge requests | ||||
|  |  | |||
|  | @ -15,8 +15,8 @@ on Atlassian cloud. To create the API token: | |||
| 1. Select **Create API token** to display a modal window with an API token. | ||||
| 1. To copy the API token, select **Copy to clipboard**, or select **View** and write | ||||
|    down the new API token. You need this value when you | ||||
|    [configure GitLab](development_panel.md#configure-gitlab). | ||||
|    [configure GitLab](configure.md). | ||||
| 
 | ||||
| You need the newly created token, and the email | ||||
| address you used when you created it, when you | ||||
| [configure GitLab](development_panel.md#configure-gitlab). | ||||
| [configure GitLab](configure.md). | ||||
|  |  | |||
|  | @ -80,4 +80,4 @@ After creating the group in Jira, grant permissions to the group by creating a p | |||
| 
 | ||||
| Write down the new Jira username and its | ||||
| password, as you need them when | ||||
| [configuring GitLab](development_panel.md#configure-gitlab). | ||||
| [configuring GitLab](configure.md). | ||||
|  |  | |||
|  | @ -67,12 +67,6 @@ Amazon S3 or Google Cloud Storage. Its features include: | |||
| 
 | ||||
| Read more on setting up and [using GitLab Managed Terraform states](../terraform_state.md) | ||||
| 
 | ||||
| WARNING: | ||||
| Like any other job artifact, Terraform plan data is [viewable by anyone with Guest access](../../permissions.md) to the repository. | ||||
| Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform plan | ||||
| includes sensitive data such as passwords, access tokens, or certificates, GitLab strongly | ||||
| recommends encrypting plan output or modifying the project visibility settings. | ||||
| 
 | ||||
| ## Terraform module registry | ||||
| 
 | ||||
| GitLab can be used as a [Terraform module registry](../../packages/terraform_module_registry/index.md) | ||||
|  |  | |||
|  | @ -15,12 +15,17 @@ you can expose details from `terraform plan` runs directly into a merge request | |||
| enabling you to see statistics about the resources that Terraform creates, | ||||
| modifies, or destroys. | ||||
| 
 | ||||
| ## Setup | ||||
| WARNING: | ||||
| Like any other job artifact, Terraform Plan data is [viewable by anyone with Guest access](../permissions.md) to the repository. | ||||
| Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform Plan | ||||
| includes sensitive data such as passwords, access tokens, or certificates, we strongly | ||||
| recommend encrypting plan output or modifying the project visibility settings. | ||||
| 
 | ||||
| ## Configure Terraform report artifacts | ||||
| 
 | ||||
| NOTE: | ||||
| GitLab ships with a [pre-built CI template](iac/index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup. | ||||
| 
 | ||||
| To manually configure a GitLab Terraform Report artifact requires the following steps: | ||||
| To manually configure a GitLab Terraform Report artifact: | ||||
| 
 | ||||
| 1. For simplicity, let's define a few reusable variables to allow us to | ||||
|    refer to these files multiple times: | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ module API | |||
|           optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' | ||||
|           optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)' | ||||
|         end | ||||
|         put ":id/invitations/:email", requirements: { email: /[^\/]+/ } do | ||||
|         put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do | ||||
|           source = find_source(source_type, params.delete(:id)) | ||||
|           invite_email = params[:email] | ||||
|           authorize_admin_source!(source_type, source) | ||||
|  | @ -88,7 +88,7 @@ module API | |||
|         params do | ||||
|           requires :email, type: String, desc: 'The email address of the invitation' | ||||
|         end | ||||
|         delete ":id/invitations/:email", requirements: { email: /[^\/]+/ } do | ||||
|         delete ":id/invitations/:email", requirements: { email: %r{[^/]+} } do | ||||
|           source = find_source(source_type, params[:id]) | ||||
|           invite_email = params[:email] | ||||
|           authorize_admin_source!(source_type, source) | ||||
|  |  | |||
|  | @ -2,4 +2,43 @@ | |||
| 
 | ||||
| module Backup | ||||
|   Error = Class.new(StandardError) | ||||
| 
 | ||||
|   class FileBackupError < Backup::Error | ||||
|     attr_reader :app_files_dir, :backup_tarball | ||||
| 
 | ||||
|     def initialize(app_files_dir, backup_tarball) | ||||
|       @app_files_dir = app_files_dir | ||||
|       @backup_tarball = backup_tarball | ||||
|     end | ||||
| 
 | ||||
|     def message | ||||
|       "Failed to create compressed file '#{backup_tarball}' when trying to backup the following paths: '#{app_files_dir}'" | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   class RepositoryBackupError < Backup::Error | ||||
|     attr_reader :container, :backup_repos_path | ||||
| 
 | ||||
|     def initialize(container, backup_repos_path) | ||||
|       @container = container | ||||
|       @backup_repos_path = backup_repos_path | ||||
|     end | ||||
| 
 | ||||
|     def message | ||||
|       "Failed to create compressed file '#{backup_repos_path}' when trying to backup the following paths: '#{container.disk_path}'" | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   class DatabaseBackupError < Backup::Error | ||||
|     attr_reader :config, :db_file_name | ||||
| 
 | ||||
|     def initialize(config, db_file_name) | ||||
|       @config = config | ||||
|       @db_file_name = db_file_name | ||||
|     end | ||||
| 
 | ||||
|     def message | ||||
|       "Failed to create compressed file '#{db_file_name}' when trying to backup the main database:\n - host: '#{config[:host]}'\n - port: '#{config[:port]}'\n - database: '#{config[:database]}'" | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,186 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module Analytics | ||||
|     module CycleAnalytics | ||||
|       class RequestParams | ||||
|         include ActiveModel::Model | ||||
|         include ActiveModel::Validations | ||||
|         include ActiveModel::Attributes | ||||
|         include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|         MAX_RANGE_DAYS = 180.days.freeze | ||||
|         DEFAULT_DATE_RANGE = 29.days # 30 including Date.today | ||||
| 
 | ||||
|         STRONG_PARAMS_DEFINITION = [ | ||||
|           :created_before, | ||||
|           :created_after, | ||||
|           :author_username, | ||||
|           :milestone_title, | ||||
|           :sort, | ||||
|           :direction, | ||||
|           :page, | ||||
|           :stage_id, | ||||
|           :end_event_filter, | ||||
|           label_name: [].freeze, | ||||
|           assignee_username: [].freeze, | ||||
|           project_ids: [].freeze | ||||
|         ].freeze | ||||
| 
 | ||||
|         FINDER_PARAM_NAMES = [ | ||||
|           :assignee_username, | ||||
|           :author_username, | ||||
|           :milestone_title, | ||||
|           :label_name | ||||
|         ].freeze | ||||
| 
 | ||||
|         attr_writer :project_ids | ||||
| 
 | ||||
|         attribute :created_after, :datetime | ||||
|         attribute :created_before, :datetime | ||||
|         attribute :group | ||||
|         attribute :current_user | ||||
|         attribute :value_stream | ||||
|         attribute :sort | ||||
|         attribute :direction | ||||
|         attribute :page | ||||
|         attribute :project | ||||
|         attribute :stage_id | ||||
|         attribute :end_event_filter | ||||
| 
 | ||||
|         FINDER_PARAM_NAMES.each do |param_name| | ||||
|           attribute param_name | ||||
|         end | ||||
| 
 | ||||
|         validates :created_after, presence: true | ||||
|         validates :created_before, presence: true | ||||
| 
 | ||||
|         validate :validate_created_before | ||||
|         validate :validate_date_range | ||||
| 
 | ||||
|         def initialize(params = {}) | ||||
|           super(params) | ||||
| 
 | ||||
|           self.created_before = (self.created_before || Time.current).at_end_of_day | ||||
|           self.created_after = (created_after || default_created_after).at_beginning_of_day | ||||
|           self.end_event_filter ||= Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder::DEFAULT_END_EVENT_FILTER | ||||
|         end | ||||
| 
 | ||||
|         def project_ids | ||||
|           Array(@project_ids) | ||||
|         end | ||||
| 
 | ||||
|         def to_data_collector_params | ||||
|           { | ||||
|             current_user: current_user, | ||||
|             from: created_after, | ||||
|             to: created_before, | ||||
|             project_ids: project_ids, | ||||
|             sort: sort&.to_sym, | ||||
|             direction: direction&.to_sym, | ||||
|             page: page, | ||||
|             end_event_filter: end_event_filter.to_sym | ||||
|           }.merge(attributes.symbolize_keys.slice(*FINDER_PARAM_NAMES)) | ||||
|         end | ||||
| 
 | ||||
|         def to_data_attributes | ||||
|           {}.tap do |attrs| | ||||
|             attrs[:group] = group_data_attributes if group | ||||
|             attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream | ||||
|             attrs[:created_after] = created_after.to_date.iso8601 | ||||
|             attrs[:created_before] = created_before.to_date.iso8601 | ||||
|             attrs[:projects] = group_projects(project_ids) if group && project_ids.present? | ||||
|             attrs[:labels] = label_name.to_json if label_name.present? | ||||
|             attrs[:assignees] = assignee_username.to_json if assignee_username.present? | ||||
|             attrs[:author] = author_username if author_username.present? | ||||
|             attrs[:milestone] = milestone_title if milestone_title.present? | ||||
|             attrs[:sort] = sort if sort.present? | ||||
|             attrs[:direction] = direction if direction.present? | ||||
|             attrs[:stage] = stage_data_attributes.to_json if stage_id.present? | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         def group_data_attributes | ||||
|           { | ||||
|             id: group.id, | ||||
|             name: group.name, | ||||
|             parent_id: group.parent_id, | ||||
|             full_path: group.full_path, | ||||
|             avatar_url: group.avatar_url | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         def value_stream_data_attributes | ||||
|           { | ||||
|             id: value_stream.id, | ||||
|             name: value_stream.name, | ||||
|             is_custom: value_stream.custom? | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         def group_projects(project_ids) | ||||
|           GroupProjectsFinder.new( | ||||
|             group: group, | ||||
|             current_user: current_user, | ||||
|             options: { include_subgroups: true }, | ||||
|             project_ids_relation: project_ids | ||||
|           ) | ||||
|             .execute | ||||
|             .with_route | ||||
|             .map { |project| project_data_attributes(project) } | ||||
|             .to_json | ||||
|         end | ||||
| 
 | ||||
|         def project_data_attributes(project) | ||||
|           { | ||||
|             id: project.to_gid.to_s, | ||||
|             name: project.name, | ||||
|             path_with_namespace: project.path_with_namespace, | ||||
|             avatar_url: project.avatar_url | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         def stage_data_attributes | ||||
|           return unless stage | ||||
| 
 | ||||
|           { | ||||
|             id: stage.id || stage.name, | ||||
|             title: stage.name | ||||
|           } | ||||
|         end | ||||
| 
 | ||||
|         def validate_created_before | ||||
|           return if created_after.nil? || created_before.nil? | ||||
| 
 | ||||
|           errors.add(:created_before, :invalid) if created_after > created_before | ||||
|         end | ||||
| 
 | ||||
|         def validate_date_range | ||||
|           return if created_after.nil? || created_before.nil? | ||||
| 
 | ||||
|           if (created_before - created_after) > MAX_RANGE_DAYS | ||||
|             errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days')) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def default_created_after | ||||
|           if created_before | ||||
|             (created_before - DEFAULT_DATE_RANGE) | ||||
|           else | ||||
|             DEFAULT_DATE_RANGE.ago | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def stage | ||||
|           return unless value_stream | ||||
| 
 | ||||
|           strong_memoize(:stage) do | ||||
|             ::Analytics::CycleAnalytics::StageFinder.new(parent: project || group, stage_id: stage_id).execute if stage_id | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -11,7 +11,7 @@ module Gitlab | |||
|             PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze | ||||
| 
 | ||||
|             def initialize(regexp) | ||||
|               super(regexp.gsub(/\\\//, '/')) | ||||
|               super(regexp.gsub(%r{\\/}, '/')) | ||||
| 
 | ||||
|               unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value) | ||||
|                 raise Lexer::SyntaxError, 'Invalid regular expression!' | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ module Gitlab | |||
|       # 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps | ||||
|       attr_reader :raw | ||||
| 
 | ||||
|       delegate :type, :content, :path, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw | ||||
|       delegate :type, :content, :path, :ancestor_path, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw | ||||
| 
 | ||||
|       def initialize(raw, merge_request:) | ||||
|         @raw = raw | ||||
|  | @ -227,6 +227,26 @@ module Gitlab | |||
|                                                      new_path: our_path) | ||||
|       end | ||||
| 
 | ||||
|       def conflict_type(diff_file) | ||||
|         if ancestor_path.present? | ||||
|           if our_path.present? && their_path.present? | ||||
|             :both_modified | ||||
|           elsif their_path.blank? | ||||
|             :modified_source_removed_target | ||||
|           else | ||||
|             :modified_target_removed_source | ||||
|           end | ||||
|         else | ||||
|           if our_path.present? && their_path.present? | ||||
|             :both_added | ||||
|           elsif their_path.blank? | ||||
|             diff_file.renamed_file? ? :renamed_same_file : :removed_target_renamed_source | ||||
|           else | ||||
|             :removed_source_renamed_target | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def map_raw_lines(raw_lines) | ||||
|  |  | |||
|  | @ -67,11 +67,11 @@ module Gitlab | |||
|             end | ||||
|           end | ||||
| 
 | ||||
|           def progress | ||||
|           def progress(current: series + 1, total: total_series, track_name: track.to_s.humanize) | ||||
|             if Gitlab.com? | ||||
|               s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize } | ||||
|               s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series.') % { current_series: current, total_series: total, track: track_name } | ||||
|             else | ||||
|               s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize, unsubscribe_link: unsubscribe_link } | ||||
|               s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { current_series: current, total_series: total, track: track_name, unsubscribe_link: unsubscribe_link } | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|  | @ -109,7 +109,7 @@ module Gitlab | |||
|           private | ||||
| 
 | ||||
|           def track | ||||
|             self.class.name.demodulize.downcase.to_sym | ||||
|             self.class.name.demodulize.underscore.to_sym | ||||
|           end | ||||
| 
 | ||||
|           def total_series | ||||
|  |  | |||
|  | @ -73,6 +73,10 @@ module Gitlab | |||
|               s_('InProductMarketing|Invite your team now') | ||||
|             ][series] | ||||
|           end | ||||
| 
 | ||||
|           def progress | ||||
|             super(current: series + 2, total: 4) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,47 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module Email | ||||
|     module Message | ||||
|       module InProductMarketing | ||||
|         class TeamShort < Base | ||||
|           def subject_line | ||||
|             s_('InProductMarketing|Team up in GitLab for greater efficiency') | ||||
|           end | ||||
| 
 | ||||
|           def tagline | ||||
|             nil | ||||
|           end | ||||
| 
 | ||||
|           def title | ||||
|             s_('InProductMarketing|Turn coworkers into collaborators') | ||||
|           end | ||||
| 
 | ||||
|           def subtitle | ||||
|             s_('InProductMarketing|Invite your team today to build better code (and processes) together') | ||||
|           end | ||||
| 
 | ||||
|           def body_line1 | ||||
|             '' | ||||
|           end | ||||
| 
 | ||||
|           def body_line2 | ||||
|             '' | ||||
|           end | ||||
| 
 | ||||
|           def cta_text | ||||
|             s_('InProductMarketing|Invite your colleagues today') | ||||
|           end | ||||
| 
 | ||||
|           def progress | ||||
|             super(total: 4, track_name: 'Team') | ||||
|           end | ||||
| 
 | ||||
|           def logo_path | ||||
|             'mailers/in_product_marketing/team-0.png' | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -6,13 +6,14 @@ module Gitlab | |||
|       class File | ||||
|         UnsupportedEncoding = Class.new(StandardError) | ||||
| 
 | ||||
|         attr_reader :their_path, :our_path, :our_mode, :repository, :commit_oid | ||||
|         attr_reader :ancestor_path, :their_path, :our_path, :our_mode, :repository, :commit_oid | ||||
| 
 | ||||
|         attr_accessor :raw_content | ||||
| 
 | ||||
|         def initialize(repository, commit_oid, conflict, raw_content) | ||||
|           @repository = repository | ||||
|           @commit_oid = commit_oid | ||||
|           @ancestor_path = conflict[:ancestor][:path] | ||||
|           @their_path = conflict[:theirs][:path] | ||||
|           @our_path = conflict[:ours][:path] | ||||
|           @our_mode = conflict[:ours][:mode] | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ module Gitlab | |||
| 
 | ||||
|       def conflict_from_gitaly_file_header(header) | ||||
|         { | ||||
|           ancestor: { path: header.ancestor_path }, | ||||
|           ours: { path: header.our_path, mode: header.our_mode }, | ||||
|           theirs: { path: header.their_path } | ||||
|         } | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ module Gitlab | |||
|         "put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500) | ||||
|       }.freeze | ||||
| 
 | ||||
|       HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze | ||||
|       HEALTH_ENDPOINT = %r{^/-/(liveness|readiness|health|metrics)/?$}.freeze | ||||
| 
 | ||||
|       FEATURE_CATEGORY_DEFAULT = 'unknown' | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ module Gitlab | |||
| 
 | ||||
|         IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze | ||||
|         DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze | ||||
|         SQL_COMMANDS_WITH_COMMENTS_REGEX = /\A(\/\*.*\*\/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$/i.freeze | ||||
|         SQL_COMMANDS_WITH_COMMENTS_REGEX = %r{\A(/\*.*\*/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$}i.freeze | ||||
| 
 | ||||
|         SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze | ||||
|         TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze | ||||
|  |  | |||
|  | @ -184,19 +184,19 @@ module Gitlab | |||
|         #   - Must not have a scheme, such as http:// or https:// | ||||
|         #   - Must not have a port number, such as :8080 or :8443 | ||||
| 
 | ||||
|         @go_package_regex ||= / | ||||
|         @go_package_regex ||= %r{ | ||||
|           \b (?# word boundary) | ||||
|           (?<domain> | ||||
|             [0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])? (?# first domain) | ||||
|             (?:\.[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])?)* (?# inner domains) | ||||
|             \.[a-z]{2,} (?# top-level domain) | ||||
|           ) | ||||
|           (?<path>\/(?: | ||||
|             [-\/$_.+!*'(),0-9a-z] (?# plain URL character) | ||||
|           (?<path>/(?: | ||||
|             [-/$_.+!*'(),0-9a-z] (?# plain URL character) | ||||
|             | %[0-9a-f]{2})* (?# URL encoded character) | ||||
|           )? (?# path) | ||||
|           \b (?# word boundary) | ||||
|         /ix.freeze | ||||
|         }ix.freeze | ||||
|       end | ||||
| 
 | ||||
|       def generic_package_version_regex | ||||
|  | @ -416,7 +416,7 @@ module Gitlab | |||
|     end | ||||
| 
 | ||||
|     def base64_regex | ||||
|       @base64_regex ||= /(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/.freeze | ||||
|       @base64_regex ||= %r{(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?}.freeze | ||||
|     end | ||||
| 
 | ||||
|     def feature_flag_regex | ||||
|  |  | |||
|  | @ -15,7 +15,8 @@ module Gitlab | |||
|       MergeRequestCounter, | ||||
|       DesignsCounter, | ||||
|       KubernetesAgentCounter, | ||||
|       StaticSiteEditorCounter | ||||
|       StaticSiteEditorCounter, | ||||
|       DiffsCounter | ||||
|     ].freeze | ||||
| 
 | ||||
|     UsageDataCounterError = Class.new(StandardError) | ||||
|  |  | |||
|  | @ -0,0 +1,10 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module UsageDataCounters | ||||
|     class DiffsCounter < BaseCounter | ||||
|       KNOWN_EVENTS = %w[searches].freeze | ||||
|       PREFIX = 'diff' | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -232,3 +232,8 @@ | |||
|   redis_slot: code_review | ||||
|   category: code_review | ||||
|   aggregation: weekly | ||||
| - name: i_code_review_user_searches_diff | ||||
|   redis_slot: code_review | ||||
|   category: code_review | ||||
|   aggregation: weekly | ||||
|   feature_flag: diff_searching_usage_data | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ module Gitlab | |||
|       return unless path.is_a?(String) | ||||
| 
 | ||||
|       path = decode_path(path) | ||||
|       path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/ | ||||
|       path_regex = %r{(\A(\.{1,2})\z|\A\.\.[/\\]|[/\\]\.\.\z|[/\\]\.\.[/\\]|\n)} | ||||
| 
 | ||||
|       if path.match?(path_regex) | ||||
|         raise PathTraversalAttackError, 'Invalid path' | ||||
|  |  | |||
|  | @ -6,6 +6,6 @@ module ProductAnalytics | |||
|     URL = Gitlab.config.gitlab.url + '/-/sp.js' | ||||
| 
 | ||||
|     # The collector URL minus protocol and /i | ||||
|     COLLECTOR_URL = Gitlab.config.gitlab.url.sub(/\Ahttps?\:\/\//, '') + '/-/collector' | ||||
|     COLLECTOR_URL = Gitlab.config.gitlab.url.sub(%r{\Ahttps?\://}, '') + '/-/collector' | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -8612,7 +8612,7 @@ msgstr "" | |||
| msgid "ContainerRegistry|Delete selected tags" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone." | ||||
| msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ContainerRegistry|Deletion disabled due to missing or insufficient permissions." | ||||
|  | @ -10918,30 +10918,30 @@ msgstr "" | |||
| msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Add a deploy token" | ||||
| msgid "DeployTokens|Allows read and write access to registry images." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows read access to the package registry." | ||||
| msgid "DeployTokens|Allows read and write access to the package registry." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows read-only access to registry images." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows read-only access to the package registry." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows read-only access to the repository." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows write access to registry images." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Allows write access to the package registry." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Copy deploy token" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Copy username" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Create deploy token" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -10954,6 +10954,15 @@ msgstr "" | |||
| msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Enter a unique name for your deploy token." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Enter an expiration date for your token. Defaults to never expire." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Expires" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -10963,7 +10972,7 @@ msgstr "" | |||
| msgid "DeployTokens|Name" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Pick a name for your unique deploy token." | ||||
| msgid "DeployTokens|New deploy token" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Revoke" | ||||
|  | @ -10984,12 +10993,6 @@ msgstr "" | |||
| msgid "DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Unless you enter a date, the token does not expire." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Unless you specify a username, it is set to \"gitlab+deploy-token-{n}\"." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -13333,6 +13336,9 @@ msgstr "" | |||
| msgid "Expiration date" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Expiration date (optional)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Expired" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -13345,9 +13351,6 @@ msgstr "" | |||
| msgid "Expires" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Expires at (optional)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Expires in %{expires_at}" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -17001,6 +17004,9 @@ msgstr "" | |||
| msgid "InProductMarketing|Invite your team now" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|Invite your team today to build better code (and processes) together" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|It's all in the stats" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -17082,6 +17088,9 @@ msgstr "" | |||
| msgid "InProductMarketing|Take your source code management to the next level" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|Team up in GitLab for greater efficiency" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|Team work makes the dream work" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -17118,6 +17127,9 @@ msgstr "" | |||
| msgid "InProductMarketing|Try it yourself" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|Turn coworkers into collaborators" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "InProductMarketing|Twitter" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -25701,9 +25713,6 @@ msgstr "" | |||
| msgid "ProjectSettings|Allow" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ProjectSettings|Allow editing commit messages" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -25731,9 +25740,6 @@ msgstr "" | |||
| msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ProjectSettings|Commit authors can edit commit messages on unprotected branches." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ProjectSettings|Configure your project resources and monitor their health." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -28655,7 +28661,7 @@ msgstr "" | |||
| msgid "Scopes" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Scopes [Select 1 or more]" | ||||
| msgid "Scopes (select at least one)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Scopes can't be blank" | ||||
|  | @ -38304,9 +38310,6 @@ msgstr "" | |||
| msgid "can only have one escalation policy" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "can't be enabled because signed commits are required for this project" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "can't be the same as the source project" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do | |||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project, group: group) } | ||||
| 
 | ||||
|   let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } } | ||||
|   let(:params) do | ||||
|     { | ||||
|       namespace_id: group, | ||||
|       project_id: project, | ||||
|       value_stream_id: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET index' do | ||||
|     context 'when user is member of the project' do | ||||
|   shared_examples 'project-level value stream analytics endpoint' do | ||||
|     before do | ||||
|       project.add_developer(user) | ||||
|     end | ||||
| 
 | ||||
|     it 'succeeds' do | ||||
|         get :index, params: params | ||||
|       get action, params: params | ||||
| 
 | ||||
|       expect(response).to have_gitlab_http_status(:ok) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'project-level value stream analytics request error examples' do | ||||
|     context 'when invalid value stream id is given' do | ||||
|       before do | ||||
|         params[:value_stream_id] = 1 | ||||
|       end | ||||
| 
 | ||||
|       it 'renders 404' do | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user is not member of the project' do | ||||
|       it 'renders 404' do | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET index' do | ||||
|     let(:action) { :index } | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics endpoint' do | ||||
|       it 'exposes the default stages' do | ||||
|         get :index, params: params | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) | ||||
|       end | ||||
|  | @ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do | |||
|             expect(list_service).to receive(:allowed?).and_return(false) | ||||
|           end | ||||
| 
 | ||||
|           get :index, params: params | ||||
|           get action, params: params | ||||
| 
 | ||||
|           expect(response).to have_gitlab_http_status(:forbidden) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when invalid value stream id is given' do | ||||
|     it_behaves_like 'project-level value stream analytics request error examples' | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET median' do | ||||
|     let(:action) { :median } | ||||
| 
 | ||||
|     before do | ||||
|         params[:value_stream_id] = 1 | ||||
|       params[:id] = 'issue' | ||||
|     end | ||||
| 
 | ||||
|       it 'renders 404' do | ||||
|         get :index, params: params | ||||
|     it_behaves_like 'project-level value stream analytics endpoint' do | ||||
|       it 'returns the median' do | ||||
|         result = 2 | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|         expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Median) do |instance| | ||||
|           expect(instance).to receive(:seconds).and_return(result) | ||||
|         end | ||||
| 
 | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(json_response['value']).to eq(result) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user is not member of the project' do | ||||
|       it 'renders 404' do | ||||
|         get :index, params: params | ||||
|     it_behaves_like 'project-level value stream analytics request error examples' | ||||
|   end | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:not_found) | ||||
|   describe 'GET average' do | ||||
|     let(:action) { :average } | ||||
| 
 | ||||
|     before do | ||||
|       params[:id] = 'issue' | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics endpoint' do | ||||
|       it 'returns the average' do | ||||
|         result = 2 | ||||
| 
 | ||||
|         expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Average) do |instance| | ||||
|           expect(instance).to receive(:seconds).and_return(result) | ||||
|         end | ||||
| 
 | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(json_response['value']).to eq(result) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics request error examples' | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET count' do | ||||
|     let(:action) { :count } | ||||
| 
 | ||||
|     before do | ||||
|       params[:id] = 'issue' | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics endpoint' do | ||||
|       it 'returns the count' do | ||||
|         count = 2 | ||||
| 
 | ||||
|         expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::DataCollector) do |instance| | ||||
|           expect(instance).to receive(:count).and_return(count) | ||||
|         end | ||||
| 
 | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(json_response['count']).to eq(count) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics request error examples' | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET records' do | ||||
|     let(:action) { :records } | ||||
| 
 | ||||
|     before do | ||||
|       params[:id] = 'issue' | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics endpoint' do | ||||
|       it 'returns the records' do | ||||
|         result = Issue.none.page(1) | ||||
| 
 | ||||
|         expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::RecordsFetcher) do |instance| | ||||
|           expect(instance).to receive(:serialized_records).and_yield(result).and_return([]) | ||||
|         end | ||||
| 
 | ||||
|         get action, params: params | ||||
| 
 | ||||
|         expect(json_response).to eq([]) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'project-level value stream analytics request error examples' | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -141,6 +141,24 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|   end | ||||
| 
 | ||||
|   describe 'GET diffs_metadata' do | ||||
|     shared_examples_for 'serializes diffs metadata with expected arguments' do | ||||
|       it 'returns success' do | ||||
|         subject | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:ok) | ||||
|       end | ||||
| 
 | ||||
|       it 'serializes paginated merge request diff collection' do | ||||
|         expect_next_instance_of(DiffsMetadataSerializer) do |instance| | ||||
|           expect(instance).to receive(:represent) | ||||
|             .with(an_instance_of(collection), expected_options) | ||||
|             .and_call_original | ||||
|         end | ||||
| 
 | ||||
|         subject | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def go(extra_params = {}) | ||||
|       params = { | ||||
|         namespace_id: project.namespace.to_param, | ||||
|  | @ -179,14 +197,12 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|     end | ||||
| 
 | ||||
|     context 'with valid diff_id' do | ||||
|       it 'returns success' do | ||||
|         go(diff_id: merge_request.merge_request_diff.id) | ||||
|       subject { go(diff_id: merge_request.merge_request_diff.id) } | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:ok) | ||||
|       end | ||||
| 
 | ||||
|       it 'serializes diffs metadata with expected arguments' do | ||||
|         expected_options = { | ||||
|       it_behaves_like 'serializes diffs metadata with expected arguments' do | ||||
|         let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff } | ||||
|         let(:expected_options) do | ||||
|           { | ||||
|             environment: nil, | ||||
|             merge_request: merge_request, | ||||
|             merge_request_diff: merge_request.merge_request_diff, | ||||
|  | @ -195,16 +211,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|             start_sha: nil, | ||||
|             commit: nil, | ||||
|             latest_diff: true, | ||||
|           only_context_commits: false | ||||
|             only_context_commits: false, | ||||
|             allow_tree_conflicts: true, | ||||
|             merge_ref_head_diff: false | ||||
|           } | ||||
| 
 | ||||
|         expect_next_instance_of(DiffsMetadataSerializer) do |instance| | ||||
|           expect(instance).to receive(:represent) | ||||
|             .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options) | ||||
|             .and_call_original | ||||
|         end | ||||
| 
 | ||||
|         go(diff_id: merge_request.merge_request_diff.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -261,14 +272,12 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|     end | ||||
| 
 | ||||
|     context 'with MR regular diff params' do | ||||
|       it 'returns success' do | ||||
|         go | ||||
|       subject { go } | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:ok) | ||||
|       end | ||||
| 
 | ||||
|       it 'serializes diffs metadata with expected arguments' do | ||||
|         expected_options = { | ||||
|       it_behaves_like 'serializes diffs metadata with expected arguments' do | ||||
|         let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff } | ||||
|         let(:expected_options) do | ||||
|           { | ||||
|             environment: nil, | ||||
|             merge_request: merge_request, | ||||
|             merge_request_diff: merge_request.merge_request_diff, | ||||
|  | @ -277,28 +286,21 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|             start_sha: nil, | ||||
|             commit: nil, | ||||
|             latest_diff: true, | ||||
|           only_context_commits: false | ||||
|             only_context_commits: false, | ||||
|             allow_tree_conflicts: true, | ||||
|             merge_ref_head_diff: nil | ||||
|           } | ||||
| 
 | ||||
|         expect_next_instance_of(DiffsMetadataSerializer) do |instance| | ||||
|           expect(instance).to receive(:represent) | ||||
|             .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options) | ||||
|             .and_call_original | ||||
|         end | ||||
| 
 | ||||
|         go | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with commit param' do | ||||
|       it 'returns success' do | ||||
|         go(commit_id: merge_request.diff_head_sha) | ||||
|       subject { go(commit_id: merge_request.diff_head_sha) } | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:ok) | ||||
|       end | ||||
| 
 | ||||
|       it 'serializes diffs metadata with expected arguments' do | ||||
|         expected_options = { | ||||
|       it_behaves_like 'serializes diffs metadata with expected arguments' do | ||||
|         let(:collection) { Gitlab::Diff::FileCollection::Commit } | ||||
|         let(:expected_options) do | ||||
|           { | ||||
|             environment: nil, | ||||
|             merge_request: merge_request, | ||||
|             merge_request_diff: nil, | ||||
|  | @ -307,16 +309,38 @@ RSpec.describe Projects::MergeRequests::DiffsController do | |||
|             start_sha: nil, | ||||
|             commit: merge_request.diff_head_commit, | ||||
|             latest_diff: nil, | ||||
|           only_context_commits: false | ||||
|             only_context_commits: false, | ||||
|             allow_tree_conflicts: true, | ||||
|             merge_ref_head_diff: nil | ||||
|           } | ||||
| 
 | ||||
|         expect_next_instance_of(DiffsMetadataSerializer) do |instance| | ||||
|           expect(instance).to receive(:represent) | ||||
|             .with(an_instance_of(Gitlab::Diff::FileCollection::Commit), expected_options) | ||||
|             .and_call_original | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|         go(commit_id: merge_request.diff_head_sha) | ||||
|     context 'when display_merge_conflicts_in_diff is disabled' do | ||||
|       subject { go } | ||||
| 
 | ||||
|       before do | ||||
|         stub_feature_flags(display_merge_conflicts_in_diff: false) | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'serializes diffs metadata with expected arguments' do | ||||
|         let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff } | ||||
|         let(:expected_options) do | ||||
|           { | ||||
|             environment: nil, | ||||
|             merge_request: merge_request, | ||||
|             merge_request_diff: merge_request.merge_request_diff, | ||||
|             merge_request_diffs: merge_request.merge_request_diffs, | ||||
|             start_version: nil, | ||||
|             start_sha: nil, | ||||
|             commit: nil, | ||||
|             latest_diff: true, | ||||
|             only_context_commits: false, | ||||
|             allow_tree_conflicts: false, | ||||
|             merge_ref_head_diff: nil | ||||
|           } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -767,8 +767,7 @@ RSpec.describe ProjectsController do | |||
|             id: project.path, | ||||
|             project: { | ||||
|               project_setting_attributes: { | ||||
|                 show_default_award_emojis: boolean_value, | ||||
|                 allow_editing_commit_messages: boolean_value | ||||
|                 show_default_award_emojis: boolean_value | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|  | @ -776,7 +775,6 @@ RSpec.describe ProjectsController do | |||
|           project.reload | ||||
| 
 | ||||
|           expect(project.show_default_award_emojis?).to eq(result) | ||||
|           expect(project.allow_editing_commit_messages?).to eq(result) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -53,6 +53,20 @@ RSpec.describe SearchController do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples_for 'support for active record query timeouts' do |action, params, method_to_stub, format| | ||||
|       before do | ||||
|         allow_next_instance_of(SearchService) do |service| | ||||
|           allow(service).to receive(method_to_stub).and_raise(ActiveRecord::QueryCanceled) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'renders a 408 when a timeout occurs' do | ||||
|         get action, params: params, format: format | ||||
| 
 | ||||
|         expect(response).to have_gitlab_http_status(:request_timeout) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'GET #show' do | ||||
|       it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do | ||||
|         it 'still allows accessing the search page' do | ||||
|  | @ -63,6 +77,7 @@ RSpec.describe SearchController do | |||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } | ||||
|       it_behaves_like 'support for active record query timeouts', :show, { search: 'hello' }, :search_objects, :html | ||||
| 
 | ||||
|       context 'uses the right partials depending on scope' do | ||||
|         using RSpec::Parameterized::TableSyntax | ||||
|  | @ -230,6 +245,7 @@ RSpec.describe SearchController do | |||
|     describe 'GET #count' do | ||||
|       it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } | ||||
|       it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } | ||||
|       it_behaves_like 'support for active record query timeouts', :count, { search: 'hello', scope: 'projects' }, :search_results, :json | ||||
| 
 | ||||
|       it 'returns the result count for the given term and scope' do | ||||
|         create(:project, :public, name: 'hello world') | ||||
|  |  | |||
|  | @ -27,7 +27,6 @@ const defaultProps = { | |||
|     emailsDisabled: false, | ||||
|     packagesEnabled: true, | ||||
|     showDefaultAwardEmojis: true, | ||||
|     allowEditingCommitMessages: false, | ||||
|   }, | ||||
|   isGitlabCom: true, | ||||
|   canDisableEmails: true, | ||||
|  | @ -53,7 +52,7 @@ describe('Settings Panel', () => { | |||
|   let wrapper; | ||||
| 
 | ||||
|   const mountComponent = ( | ||||
|     { currentSettings = {}, glFeatures = {}, ...customProps } = {}, | ||||
|     { currentSettings = {}, ...customProps } = {}, | ||||
|     mountFn = shallowMount, | ||||
|   ) => { | ||||
|     const propsData = { | ||||
|  | @ -64,9 +63,6 @@ describe('Settings Panel', () => { | |||
| 
 | ||||
|     return mountFn(settingsPanel, { | ||||
|       propsData, | ||||
|       provide: { | ||||
|         glFeatures, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|  | @ -100,8 +96,6 @@ describe('Settings Panel', () => { | |||
|   const findShowDefaultAwardEmojis = () => | ||||
|     wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]'); | ||||
|   const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' }); | ||||
|   const findAllowEditingCommitMessages = () => | ||||
|     wrapper.find({ ref: 'allow-editing-commit-messages' }).exists(); | ||||
|   const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|  | @ -582,18 +576,6 @@ describe('Settings Panel', () => { | |||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Settings panel with feature flags', () => { | ||||
|     describe('Allow edit of commit message', () => { | ||||
|       it('should show the allow editing of commit messages checkbox', () => { | ||||
|         wrapper = mountComponent({ | ||||
|           glFeatures: { allowEditingCommitMessages: true }, | ||||
|         }); | ||||
| 
 | ||||
|         expect(findAllowEditingCommitMessages()).toBe(true); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Analytics', () => { | ||||
|     it('should show the analytics toggle', () => { | ||||
|       wrapper = mountComponent(); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { GlSprintf } from '@gitlab/ui'; | ||||
| import { GlSprintf, GlFormInput } from '@gitlab/ui'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { nextTick } from 'vue'; | ||||
| import component from '~/registry/explorer/components/details_page/delete_modal.vue'; | ||||
| import { | ||||
|   REMOVE_TAG_CONFIRMATION_TEXT, | ||||
|  | @ -12,8 +13,9 @@ import { GlModal } from '../../stubs'; | |||
| describe('Delete Modal', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const findModal = () => wrapper.find(GlModal); | ||||
|   const findModal = () => wrapper.findComponent(GlModal); | ||||
|   const findDescription = () => wrapper.find('[data-testid="description"]'); | ||||
|   const findInputComponent = () => wrapper.findComponent(GlFormInput); | ||||
| 
 | ||||
|   const mountComponent = (propsData) => { | ||||
|     wrapper = shallowMount(component, { | ||||
|  | @ -25,6 +27,13 @@ describe('Delete Modal', () => { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const expectPrimaryActionStatus = (disabled = true) => | ||||
|     expect(findModal().props('actionPrimary')).toMatchObject( | ||||
|       expect.objectContaining({ | ||||
|         attributes: [{ variant: 'danger' }, { disabled }], | ||||
|       }), | ||||
|     ); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|  | @ -65,11 +74,49 @@ describe('Delete Modal', () => { | |||
|     it('has the correct description', () => { | ||||
|       mountComponent({ deleteImage: true }); | ||||
| 
 | ||||
|       expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TEXT); | ||||
|       expect(wrapper.text()).toContain( | ||||
|         DELETE_IMAGE_CONFIRMATION_TEXT.replace('%{code}', '').trim(), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     describe('delete button', () => { | ||||
|       const itemsToBeDeleted = [{ project: { path: 'foo' } }]; | ||||
| 
 | ||||
|       it('is disabled by default', () => { | ||||
|         mountComponent({ deleteImage: true }); | ||||
| 
 | ||||
|         expectPrimaryActionStatus(); | ||||
|       }); | ||||
| 
 | ||||
|       it('if the user types something different from the project path is disabled', async () => { | ||||
|         mountComponent({ deleteImage: true, itemsToBeDeleted }); | ||||
| 
 | ||||
|         findInputComponent().vm.$emit('input', 'bar'); | ||||
| 
 | ||||
|         await nextTick(); | ||||
| 
 | ||||
|         expectPrimaryActionStatus(); | ||||
|       }); | ||||
| 
 | ||||
|       it('if the user types the project path it is enabled', async () => { | ||||
|         mountComponent({ deleteImage: true, itemsToBeDeleted }); | ||||
| 
 | ||||
|         findInputComponent().vm.$emit('input', 'foo'); | ||||
| 
 | ||||
|         await nextTick(); | ||||
| 
 | ||||
|         expectPrimaryActionStatus(false); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('when we are deleting tags', () => { | ||||
|     it('delete button is enabled', () => { | ||||
|       mountComponent(); | ||||
| 
 | ||||
|       expectPrimaryActionStatus(false); | ||||
|     }); | ||||
| 
 | ||||
|     describe('itemsToBeDeleted contains one element', () => { | ||||
|       beforeEach(() => { | ||||
|         mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] }); | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| import { GlButton, GlIcon } from '@gitlab/ui'; | ||||
| import { GlDropdownItem, GlIcon } from '@gitlab/ui'; | ||||
| import { shallowMount, createLocalVue } from '@vue/test-utils'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
| import { useFakeDate } from 'helpers/fake_date'; | ||||
| import createMockApollo from 'helpers/mock_apollo_helper'; | ||||
| import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { GlDropdown } from 'jest/registry/explorer/stubs'; | ||||
| import component from '~/registry/explorer/components/details_page/details_header.vue'; | ||||
| import { | ||||
|   UNSCHEDULED_STATUS, | ||||
|  | @ -48,8 +49,8 @@ describe('Details Header', () => { | |||
|   const findTitle = () => findByTestId('title'); | ||||
|   const findTagsCount = () => findByTestId('tags-count'); | ||||
|   const findCleanup = () => findByTestId('cleanup'); | ||||
|   const findDeleteButton = () => wrapper.find(GlButton); | ||||
|   const findInfoIcon = () => wrapper.find(GlIcon); | ||||
|   const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); | ||||
|   const findInfoIcon = () => wrapper.findComponent(GlIcon); | ||||
| 
 | ||||
|   const waitForMetadataItems = async () => { | ||||
|     // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
 | ||||
|  | @ -84,6 +85,8 @@ describe('Details Header', () => { | |||
|       mocks, | ||||
|       stubs: { | ||||
|         TitleArea, | ||||
|         GlDropdown, | ||||
|         GlDropdownItem, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | @ -152,10 +155,11 @@ describe('Details Header', () => { | |||
|     it('has the correct props', () => { | ||||
|       mountComponent(); | ||||
| 
 | ||||
|       expect(findDeleteButton().props()).toMatchObject({ | ||||
|       expect(findDeleteButton().attributes()).toMatchObject( | ||||
|         expect.objectContaining({ | ||||
|           variant: 'danger', | ||||
|         disabled: false, | ||||
|       }); | ||||
|         }), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('emits the correct event', () => { | ||||
|  | @ -168,16 +172,16 @@ describe('Details Header', () => { | |||
| 
 | ||||
|     it.each` | ||||
|       canDelete | disabled | isDisabled | ||||
|       ${true}   | ${false} | ${false} | ||||
|       ${true}   | ${true}  | ${true} | ||||
|       ${false}  | ${false} | ${true} | ||||
|       ${false}  | ${true}  | ${true} | ||||
|       ${true}   | ${false} | ${undefined} | ||||
|       ${true}   | ${true}  | ${'true'} | ||||
|       ${false}  | ${false} | ${'true'} | ||||
|       ${false}  | ${true}  | ${'true'} | ||||
|     `(
 | ||||
|       'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled', | ||||
|       ({ canDelete, disabled, isDisabled }) => { | ||||
|         mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } }); | ||||
| 
 | ||||
|         expect(findDeleteButton().props('disabled')).toBe(isDisabled); | ||||
|         expect(findDeleteButton().attributes('disabled')).toBe(isDisabled); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
|  |  | |||
|  | @ -119,6 +119,7 @@ export const containerRepositoryMock = { | |||
|   expirationPolicyCleanupStatus: 'UNSCHEDULED', | ||||
|   project: { | ||||
|     visibility: 'public', | ||||
|     path: 'gitlab-test', | ||||
|     containerExpirationPolicy: { | ||||
|       enabled: false, | ||||
|       nextRunAt: '2020-11-27T08:59:27Z', | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { | |||
|   GlModal as RealGlModal, | ||||
|   GlEmptyState as RealGlEmptyState, | ||||
|   GlSkeletonLoader as RealGlSkeletonLoader, | ||||
|   GlDropdown as RealGlDropdown, | ||||
| } from '@gitlab/ui'; | ||||
| import { RouterLinkStub } from '@vue/test-utils'; | ||||
| import { stubComponent } from 'helpers/stub_component'; | ||||
|  | @ -38,3 +39,7 @@ export const ListItem = { | |||
|     }; | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export const GlDropdown = stubComponent(RealGlDropdown, { | ||||
|   template: '<div><slot></slot></div>', | ||||
| }); | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe ::Mutations::BaseMutation do | ||||
|   include GraphqlHelpers | ||||
| 
 | ||||
|   describe 'argument nullability' do | ||||
|     let_it_be(:user) { create(:user) } | ||||
|     let_it_be(:context)  { { current_user: user } } | ||||
| 
 | ||||
|     subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) } | ||||
| 
 | ||||
|     describe 'when using a mutation with correct argument declarations' do | ||||
|       context 'when argument is nullable and required' do | ||||
|         let(:mutation_class) do | ||||
|           Class.new(described_class) do | ||||
|             argument :foo, GraphQL::STRING_TYPE, required: :nullable | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready? }.to raise_error(ArgumentError, /must be provided: foo/) | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready?(foo: nil) }.not_to raise_error | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready?(foo: "bar") }.not_to raise_error | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when argument is required and NOT nullable' do | ||||
|         let(:mutation_class) do | ||||
|           Class.new(described_class) do | ||||
|             argument :foo, GraphQL::STRING_TYPE, required: true | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready? }.to raise_error(ArgumentError, /must be provided/) | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready?(foo: nil) }.to raise_error(ArgumentError, /must be provided/) | ||||
|         end | ||||
| 
 | ||||
|         specify do | ||||
|           expect { subject.ready?(foo: "bar") }.not_to raise_error | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -41,7 +41,7 @@ RSpec.describe Mutations::Ci::Runner::Delete do | |||
|       let(:mutation_params) { {} } | ||||
| 
 | ||||
|       it 'raises an error' do | ||||
|         expect { subject }.to raise_error(ArgumentError, "missing keyword: :id") | ||||
|         expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ RSpec.describe Mutations::Ci::Runner::Update do | |||
|       let(:mutation_params) { {} } | ||||
| 
 | ||||
|       it 'raises an error' do | ||||
|         expect { subject }.to raise_error(ArgumentError, "missing keyword: :id") | ||||
|         expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Types::BaseArgument do | ||||
|   include_examples 'Gitlab-style deprecations' do | ||||
|   let_it_be(:field) do | ||||
|     Types::BaseField.new(name: 'field', type: String, null: true) | ||||
|   end | ||||
|  | @ -13,5 +12,32 @@ RSpec.describe Types::BaseArgument do | |||
|   def subject(args = {}) | ||||
|     described_class.new(**base_args.merge(args)) | ||||
|   end | ||||
| 
 | ||||
|   include_examples 'Gitlab-style deprecations' | ||||
| 
 | ||||
|   describe 'required argument declarations' do | ||||
|     it 'accepts nullable, required arguments' do | ||||
|       arguments = base_args.merge({ required: :nullable }) | ||||
| 
 | ||||
|       expect { subject(arguments) }.not_to raise_error | ||||
|     end | ||||
| 
 | ||||
|     it 'accepts required, non-nullable arguments' do | ||||
|       arguments = base_args.merge({ required: true }) | ||||
| 
 | ||||
|       expect { subject(arguments) }.not_to raise_error | ||||
|     end | ||||
| 
 | ||||
|     it 'accepts non-required arguments' do | ||||
|       arguments = base_args.merge({ required: false }) | ||||
| 
 | ||||
|       expect { subject(arguments) }.not_to raise_error | ||||
|     end | ||||
| 
 | ||||
|     it 'accepts no required argument declaration' do | ||||
|       arguments = base_args | ||||
| 
 | ||||
|       expect { subject(arguments) }.not_to raise_error | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -917,34 +917,4 @@ RSpec.describe ProjectsHelper do | |||
|       subject | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#project_permissions_settings' do | ||||
|     context 'with no project_setting associated' do | ||||
|       it 'includes a value for edit commit messages' do | ||||
|         settings = project_permissions_settings(project) | ||||
| 
 | ||||
|         expect(settings[:allowEditingCommitMessages]).to be_falsy | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when commits are allowed to be edited' do | ||||
|       it 'includes the edit commit message value' do | ||||
|         project.create_project_setting(allow_editing_commit_messages: true) | ||||
| 
 | ||||
|         settings = project_permissions_settings(project) | ||||
| 
 | ||||
|         expect(settings[:allowEditingCommitMessages]).to be_truthy | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when commits are not allowed to be edited' do | ||||
|       it 'returns false to the edit commit message value' do | ||||
|         project.create_project_setting(allow_editing_commit_messages: false) | ||||
| 
 | ||||
|         settings = project_permissions_settings(project) | ||||
| 
 | ||||
|         expect(settings[:allowEditingCommitMessages]).to be_falsy | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Backup::DatabaseBackupError do | ||||
|   let(:config) do | ||||
|     { | ||||
|       host: 'localhost', | ||||
|       port: 5432, | ||||
|       database: 'gitlabhq_test' | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   let(:db_file_name) { File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') } | ||||
| 
 | ||||
|   subject { described_class.new(config, db_file_name) } | ||||
| 
 | ||||
|   it { is_expected.to respond_to :config } | ||||
|   it { is_expected.to respond_to :db_file_name } | ||||
| 
 | ||||
|   it 'expects exception message to include database file' do | ||||
|     expect(subject.message).to include("#{db_file_name}") | ||||
|   end | ||||
| 
 | ||||
|   it 'expects exception message to include database paths being back-up' do | ||||
|     expect(subject.message).to include("#{config[:host]}") | ||||
|     expect(subject.message).to include("#{config[:port]}") | ||||
|     expect(subject.message).to include("#{config[:database]}") | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,35 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Backup::FileBackupError do | ||||
|   let_it_be(:lfs) { create(:lfs_object) } | ||||
|   let_it_be(:upload) { create(:upload) } | ||||
| 
 | ||||
|   let(:backup_tarball) { '/tmp/backup/uploads' } | ||||
| 
 | ||||
|   shared_examples 'includes backup path' do | ||||
|     it { is_expected.to respond_to :app_files_dir } | ||||
|     it { is_expected.to respond_to :backup_tarball } | ||||
| 
 | ||||
|     it 'expects exception message to include file backup path location' do | ||||
|       expect(subject.message).to include("#{subject.backup_tarball}") | ||||
|     end | ||||
| 
 | ||||
|     it 'expects exception message to include file being back-up' do | ||||
|       expect(subject.message).to include("#{subject.app_files_dir}") | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with lfs file' do | ||||
|     subject { described_class.new(lfs, backup_tarball) } | ||||
| 
 | ||||
|     it_behaves_like 'includes backup path' | ||||
|   end | ||||
| 
 | ||||
|   context 'with uploads file' do | ||||
|     subject { described_class.new(upload, backup_tarball) } | ||||
| 
 | ||||
|     it_behaves_like 'includes backup path' | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,42 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Backup::RepositoryBackupError do | ||||
|   let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') } | ||||
|   let_it_be(:project) { create(:project, :repository) } | ||||
|   let_it_be(:wiki) { ProjectWiki.new(project, nil ) } | ||||
| 
 | ||||
|   let(:backup_repos_path) { '/tmp/backup/repositories' } | ||||
| 
 | ||||
|   shared_examples 'includes backup path' do | ||||
|     it { is_expected.to respond_to :container } | ||||
|     it { is_expected.to respond_to :backup_repos_path } | ||||
| 
 | ||||
|     it 'expects exception message to include repo backup path location' do | ||||
|       expect(subject.message).to include("#{subject.backup_repos_path}") | ||||
|     end | ||||
| 
 | ||||
|     it 'expects exception message to include container being back-up' do | ||||
|       expect(subject.message).to include("#{subject.container.disk_path}") | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with snippet repository' do | ||||
|     subject { described_class.new(snippet, backup_repos_path) } | ||||
| 
 | ||||
|     it_behaves_like 'includes backup path' | ||||
|   end | ||||
| 
 | ||||
|   context 'with project repository' do | ||||
|     subject { described_class.new(project, backup_repos_path) } | ||||
| 
 | ||||
|     it_behaves_like 'includes backup path' | ||||
|   end | ||||
| 
 | ||||
|   context 'with wiki repository' do | ||||
|     subject { described_class.new(wiki, backup_repos_path) } | ||||
| 
 | ||||
|     it_behaves_like 'includes backup path' | ||||
|   end | ||||
| end | ||||
|  | @ -18,7 +18,15 @@ RSpec.describe Gitlab::Conflict::File do | |||
|   let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) } | ||||
| 
 | ||||
|   describe 'delegates' do | ||||
|     it { expect(conflict_file).to delegate_method(:type).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:content).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:path).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:ancestor_path).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:their_path).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:our_path).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:our_mode).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:our_blob).to(:raw) } | ||||
|     it { expect(conflict_file).to delegate_method(:repository).to(:raw) } | ||||
|   end | ||||
| 
 | ||||
|   describe '#resolve_lines' do | ||||
|  | @ -328,4 +336,27 @@ RSpec.describe Gitlab::Conflict::File do | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#conflict_type' do | ||||
|     using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|     let(:rugged_conflict) { { ancestor: { path: ancestor_path }, theirs: { path: their_path }, ours: { path: our_path } } } | ||||
|     let(:diff_file) { double(renamed_file?: renamed_file?) } | ||||
| 
 | ||||
|     subject(:conflict_type) { conflict_file.conflict_type(diff_file) } | ||||
| 
 | ||||
|     where(:ancestor_path, :their_path, :our_path, :renamed_file?, :result) do | ||||
|       '/ancestor/path' | '/their/path' | '/our/path' | false | :both_modified | ||||
|       '/ancestor/path' | ''            | '/our/path' | false | :modified_source_removed_target | ||||
|       '/ancestor/path' | '/their/path' | ''          | false | :modified_target_removed_source | ||||
|       ''               | '/their/path' | '/our/path' | false | :both_added | ||||
|       ''               | ''            | '/our/path' | false | :removed_target_renamed_source | ||||
|       ''               | ''            | '/our/path' | true  | :renamed_same_file | ||||
|       ''               | '/their/path' | ''          | false | :removed_source_renamed_target | ||||
|     end | ||||
| 
 | ||||
|     with_them do | ||||
|       it { expect(conflict_type).to eq(result) } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,47 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Email::Message::InProductMarketing::TeamShort do | ||||
|   using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|   let_it_be(:group) { build(:group) } | ||||
|   let_it_be(:user) { build(:user) } | ||||
| 
 | ||||
|   let(:series) { 0 } | ||||
| 
 | ||||
|   subject(:message) { described_class.new(group: group, user: user, series: series)} | ||||
| 
 | ||||
|   describe 'public methods' do | ||||
|     it 'returns value for series', :aggregate_failures do | ||||
|       expect(message.subject_line).to eq 'Team up in GitLab for greater efficiency' | ||||
|       expect(message.tagline).to be_nil | ||||
|       expect(message.title).to eq 'Turn coworkers into collaborators' | ||||
|       expect(message.subtitle).to eq 'Invite your team today to build better code (and processes) together' | ||||
|       expect(message.body_line1).to be_empty | ||||
|       expect(message.body_line2).to be_empty | ||||
|       expect(message.cta_text).to eq 'Invite your colleagues today' | ||||
|       expect(message.logo_path).to eq 'mailers/in_product_marketing/team-0.png' | ||||
|     end | ||||
| 
 | ||||
|     describe '#progress' do | ||||
|       subject { message.progress } | ||||
| 
 | ||||
|       before do | ||||
|         allow(Gitlab).to receive(:com?).and_return(is_gitlab_com) | ||||
|       end | ||||
| 
 | ||||
|       context 'on gitlab.com' do | ||||
|         let(:is_gitlab_com) { true } | ||||
| 
 | ||||
|         it { is_expected.to include('This is email 1 of 4 in the Team series') } | ||||
|       end | ||||
| 
 | ||||
|       context 'not on gitlab.com' do | ||||
|         let(:is_gitlab_com) { false } | ||||
| 
 | ||||
|         it { is_expected.to include('This is email 1 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -23,6 +23,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do | |||
|         expect(message.body_line2).to be_present | ||||
|         expect(message.cta_text).to be_present | ||||
|       end | ||||
| 
 | ||||
|       describe '#progress' do | ||||
|         subject { message.progress } | ||||
| 
 | ||||
|         before do | ||||
|           allow(Gitlab).to receive(:com?).and_return(is_gitlab_com) | ||||
|         end | ||||
| 
 | ||||
|         context 'on gitlab.com' do | ||||
|           let(:is_gitlab_com) { true } | ||||
| 
 | ||||
|           it { is_expected.to include("This is email #{series + 2} of 4 in the Team series") } | ||||
|         end | ||||
| 
 | ||||
|         context 'not on gitlab.com' do | ||||
|           let(:is_gitlab_com) { false } | ||||
| 
 | ||||
|           it { is_expected.to include("This is email #{series + 2} of 4 in the Team series", Gitlab::Routing.url_helpers.profile_notifications_url) } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with series 2' do | ||||
|  | @ -37,6 +57,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do | |||
|         expect(message.body_line2).to be_present | ||||
|         expect(message.cta_text).to be_present | ||||
|       end | ||||
| 
 | ||||
|       describe '#progress' do | ||||
|         subject { message.progress } | ||||
| 
 | ||||
|         before do | ||||
|           allow(Gitlab).to receive(:com?).and_return(is_gitlab_com) | ||||
|         end | ||||
| 
 | ||||
|         context 'on gitlab.com' do | ||||
|           let(:is_gitlab_com) { true } | ||||
| 
 | ||||
|           it { is_expected.to include('This is email 4 of 4 in the Team series') } | ||||
|         end | ||||
| 
 | ||||
|         context 'not on gitlab.com' do | ||||
|           let(:is_gitlab_com) { false } | ||||
| 
 | ||||
|           it { is_expected.to include('This is email 4 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue