Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									1bb7f81e23
								
							
						
					
					
						commit
						7f3f19582b
					
				|  | @ -1,6 +1,5 @@ | |||
| import Vue from 'vue'; | ||||
| import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; | ||||
| import { formatTimezone } from '~/lib/utils/datetime_utility'; | ||||
| 
 | ||||
| export const initTimezoneDropdown = () => { | ||||
|   const el = document.querySelector('.js-timezone-dropdown'); | ||||
|  | @ -11,15 +10,12 @@ export const initTimezoneDropdown = () => { | |||
| 
 | ||||
|   const { timezoneData, initialValue } = el.dataset; | ||||
|   const timezones = JSON.parse(timezoneData); | ||||
|   const initialTimezone = initialValue | ||||
|     ? formatTimezone(timezones.find((timezone) => timezone.identifier === initialValue)) | ||||
|     : undefined; | ||||
| 
 | ||||
|   const timezoneDropdown = new Vue({ | ||||
|     el, | ||||
|     data() { | ||||
|       return { | ||||
|         value: initialTimezone, | ||||
|         value: initialValue, | ||||
|       }; | ||||
|     }, | ||||
|     render(h) { | ||||
|  |  | |||
|  | @ -1,3 +1,8 @@ | |||
| import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app'; | ||||
| import initForm from '../shared/init_form'; | ||||
| 
 | ||||
| initForm(); | ||||
| if (gon.features?.pipelineSchedulesVue) { | ||||
|   initPipelineSchedulesFormApp('#pipeline-schedules-form-edit'); | ||||
| } else { | ||||
|   initForm(); | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import Vue from 'vue'; | ||||
| import { BV_SHOW_MODAL } from '~/lib/utils/constants'; | ||||
| import initPipelineSchedulesApp from '~/pipeline_schedules/mount_pipeline_schedules_app'; | ||||
| import PipelineSchedulesTakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue'; | ||||
| import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; | ||||
| 
 | ||||
| function initPipelineSchedules() { | ||||
| function initPipelineSchedulesCallout() { | ||||
|   const el = document.getElementById('pipeline-schedules-callout'); | ||||
| 
 | ||||
|   if (!el) { | ||||
|  | @ -15,6 +16,7 @@ function initPipelineSchedules() { | |||
|   // eslint-disable-next-line no-new
 | ||||
|   new Vue({ | ||||
|     el, | ||||
|     name: 'PipelineSchedulesCalloutRoot', | ||||
|     provide: { | ||||
|       docsUrl, | ||||
|       illustrationUrl, | ||||
|  | @ -25,6 +27,8 @@ function initPipelineSchedules() { | |||
|   }); | ||||
| } | ||||
| 
 | ||||
| // TODO: move take ownership feature into new Vue app
 | ||||
| // located in directory app/assets/javascripts/pipeline_schedules/components
 | ||||
| function initTakeownershipModal() { | ||||
|   const modalId = 'pipeline-take-ownership-modal'; | ||||
|   const buttonSelector = 'js-take-ownership-button'; | ||||
|  | @ -63,5 +67,10 @@ function initTakeownershipModal() { | |||
|   }); | ||||
| } | ||||
| 
 | ||||
| initPipelineSchedules(); | ||||
| initTakeownershipModal(); | ||||
| initPipelineSchedulesCallout(); | ||||
| 
 | ||||
| if (gon.features?.pipelineSchedulesVue) { | ||||
|   initPipelineSchedulesApp(); | ||||
| } else { | ||||
|   initTakeownershipModal(); | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,8 @@ | |||
| import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app'; | ||||
| import initForm from '../shared/init_form'; | ||||
| 
 | ||||
| initForm(); | ||||
| if (gon.features?.pipelineSchedulesVue) { | ||||
|   initPipelineSchedulesFormApp('#pipeline-schedules-form-new'); | ||||
| } else { | ||||
|   initForm(); | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| <script> | ||||
| import PipelineSchedulesTable from './table/pipeline_schedules_table.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     PipelineSchedulesTable, | ||||
|   }, | ||||
|   inject: { | ||||
|     fullPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <pipeline-schedules-table /> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -0,0 +1,18 @@ | |||
| <script> | ||||
| import { GlForm } from '@gitlab/ui'; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     GlForm, | ||||
|   }, | ||||
|   inject: { | ||||
|     fullPath: { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <gl-form /> | ||||
| </template> | ||||
|  | @ -0,0 +1,13 @@ | |||
| <script> | ||||
| import { GlTableLite } from '@gitlab/ui'; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     GlTableLite, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <gl-table-lite /> | ||||
| </template> | ||||
|  | @ -0,0 +1,32 @@ | |||
| import Vue from 'vue'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
| import createDefaultClient from '~/lib/graphql'; | ||||
| import PipelineSchedules from './components/pipeline_schedules.vue'; | ||||
| 
 | ||||
| Vue.use(VueApollo); | ||||
| 
 | ||||
| const apolloProvider = new VueApollo({ | ||||
|   defaultClient: createDefaultClient(), | ||||
| }); | ||||
| 
 | ||||
| export default () => { | ||||
|   const containerEl = document.querySelector('#pipeline-schedules-app'); | ||||
| 
 | ||||
|   if (!containerEl) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   const { fullPath } = containerEl.dataset; | ||||
| 
 | ||||
|   return new Vue({ | ||||
|     el: containerEl, | ||||
|     name: 'PipelineSchedulesRoot', | ||||
|     apolloProvider, | ||||
|     provide: { | ||||
|       fullPath, | ||||
|     }, | ||||
|     render(createElement) { | ||||
|       return createElement(PipelineSchedules); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | @ -0,0 +1,32 @@ | |||
| import Vue from 'vue'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
| import createDefaultClient from '~/lib/graphql'; | ||||
| import PipelineSchedulesForm from './components/pipeline_schedules_form.vue'; | ||||
| 
 | ||||
| Vue.use(VueApollo); | ||||
| 
 | ||||
| const apolloProvider = new VueApollo({ | ||||
|   defaultClient: createDefaultClient(), | ||||
| }); | ||||
| 
 | ||||
| export default (selector) => { | ||||
|   const containerEl = document.querySelector(selector); | ||||
| 
 | ||||
|   if (!containerEl) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   const { fullPath } = containerEl.dataset; | ||||
| 
 | ||||
|   return new Vue({ | ||||
|     el: containerEl, | ||||
|     name: 'PipelineSchedulesFormRoot', | ||||
|     apolloProvider, | ||||
|     provide: { | ||||
|       fullPath, | ||||
|     }, | ||||
|     render(createElement) { | ||||
|       return createElement(PipelineSchedulesForm); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | @ -34,7 +34,7 @@ export default { | |||
|   data() { | ||||
|     return { | ||||
|       searchTerm: '', | ||||
|       tzValue: this.value, | ||||
|       tzValue: this.initialTimezone(this.timezoneData, this.value), | ||||
|     }; | ||||
|   }, | ||||
|   translations: { | ||||
|  | @ -71,12 +71,31 @@ export default { | |||
|     isSelected(timezone) { | ||||
|       return this.tzValue === timezone.formattedTimezone; | ||||
|     }, | ||||
|     initialTimezone(timezones, value) { | ||||
|       if (!value) { | ||||
|         return undefined; | ||||
|       } | ||||
| 
 | ||||
|       const initialTimezone = timezones.find((timezone) => timezone.identifier === value); | ||||
| 
 | ||||
|       if (initialTimezone) { | ||||
|         return formatTimezone(initialTimezone); | ||||
|       } | ||||
| 
 | ||||
|       return undefined; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div> | ||||
|     <input v-if="name" id="user_timezone" :name="name" :value="timezoneIdentifier" type="hidden" /> | ||||
|     <input | ||||
|       v-if="name" | ||||
|       id="user_timezone" | ||||
|       :name="name" | ||||
|       :value="timezoneIdentifier || value" | ||||
|       type="hidden" | ||||
|     /> | ||||
|     <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!" v-bind="$attrs"> | ||||
|       <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus /> | ||||
|       <gl-dropdown-item | ||||
|  |  | |||
|  | @ -735,13 +735,7 @@ | |||
|     } | ||||
| 
 | ||||
|     .issue-check { | ||||
|       padding-right: $gl-padding; | ||||
|       margin-bottom: 10px; | ||||
|       min-width: 15px; | ||||
| 
 | ||||
|       .selected-issuable { | ||||
|         vertical-align: text-top; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .issuable-milestone, | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController | |||
|   before_action :authorize_update_pipeline_schedule!, only: [:edit, :update] | ||||
|   before_action :authorize_take_ownership_pipeline_schedule!, only: [:take_ownership] | ||||
|   before_action :authorize_admin_pipeline_schedule!, only: [:destroy] | ||||
|   before_action :push_schedule_feature_flag, only: [:index, :new, :edit] | ||||
| 
 | ||||
|   feature_category :continuous_integration | ||||
|   urgency :low | ||||
|  | @ -115,4 +116,8 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController | |||
|   def authorize_admin_pipeline_schedule! | ||||
|     return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule) | ||||
|   end | ||||
| 
 | ||||
|   def push_schedule_feature_flag | ||||
|     push_frontend_feature_flag(:pipeline_schedules_vue, @project) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -447,6 +447,10 @@ module ApplicationHelper | |||
|     form_for(record, *(args << options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder })), &block) | ||||
|   end | ||||
| 
 | ||||
|   def gitlab_ui_form_with(**args, &block) | ||||
|     form_with(**args.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder }), &block) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def appearance | ||||
|  |  | |||
|  | @ -383,10 +383,12 @@ module Issuable | |||
|         milestone_table = Milestone.arel_table | ||||
|         grouping_columns << milestone_table[:id] | ||||
|         grouping_columns << milestone_table[:due_date] | ||||
|       elsif %w(merged_at_desc merged_at_asc).include?(sort) | ||||
|       elsif %w(merged_at_desc merged_at_asc merged_at).include?(sort) | ||||
|         grouping_columns << MergeRequest::Metrics.arel_table[:id] | ||||
|         grouping_columns << MergeRequest::Metrics.arel_table[:merged_at] | ||||
|       elsif %w(closed_at_desc closed_at_asc).include?(sort) | ||||
|         grouping_columns << MergeRequest::Metrics.arel_table[:closed_at] | ||||
|       elsif %w(closed_at_desc closed_at_asc closed_at).include?(sort) | ||||
|         grouping_columns << MergeRequest::Metrics.arel_table[:id] | ||||
|         grouping_columns << MergeRequest::Metrics.arel_table[:latest_closed_at] | ||||
|       end | ||||
| 
 | ||||
|       grouping_columns | ||||
|  |  | |||
|  | @ -10,10 +10,10 @@ | |||
|     = label_tag :password | ||||
|     = password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true } | ||||
|   - if !hide_remember_me && devise_mapping.rememberable? | ||||
|     .remember-me.gl-px-5 | ||||
|       %label{ for: "remember_me" } | ||||
|         = check_box_tag :remember_me, '1', false, id: 'remember_me' | ||||
|         %span= _('Remember me') | ||||
|     .gl-px-5 | ||||
|       = render Pajamas::CheckboxTagComponent.new(name: 'remember_me') do |c| | ||||
|         = c.label do | ||||
|           = _('Remember me') | ||||
| 
 | ||||
|   .submit-container.move-submit-down.gl-px-5.gl-pb-5 | ||||
|     = submit_tag submit_message, class: "gl-button btn btn-confirm", data: { qa_selector: 'sign_in_button' } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| = form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'group-form gl-show-field-errors' do |f| | ||||
|   .gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5 | ||||
| = gitlab_ui_form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'gl-show-field-errors' do |f| | ||||
|   .gl-border-l-solid.gl-border-r-solid.gl-border-t-solid.gl-border-gray-100.gl-border-1.gl-p-5.gl-mt-4 | ||||
|     .gl-display-flex.gl-align-items-center | ||||
|       %h4.gl-display-flex | ||||
|         = s_('GroupsNew|Import groups from another instance of GitLab') | ||||
|  | @ -32,4 +32,4 @@ | |||
|         id: 'import_gitlab_token', | ||||
|         data: { qa_selector: 'import_gitlab_token' } | ||||
|   .gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5 | ||||
|     = f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'connect_instance_button' } | ||||
|     = f.submit s_('GroupsNew|Connect instance'), pajamas_button: true, data: { qa_selector: 'connect_instance_button' } | ||||
|  |  | |||
|  | @ -1,10 +1,5 @@ | |||
| %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue_container', qa_issue_title: issue.title } } | ||||
|   .issuable-info-container | ||||
|     - if @can_bulk_update | ||||
|       .issue-check.hidden | ||||
|         - checkbox_id = dom_id(issue, "selected") | ||||
|         %label.gl-sr-only{ for: checkbox_id }= issue.title | ||||
|         = check_box_tag checkbox_id, nil, false, 'data-id' => issue.id, class: "selected-issuable" | ||||
|     .issuable-main-info | ||||
|       .issue-title.title | ||||
|         %span.issue-title-text.js-onboarding-issue-item{ dir: "auto" } | ||||
|  |  | |||
|  | @ -1,9 +1,12 @@ | |||
| %li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } | ||||
|   - if @can_bulk_update | ||||
|     .issue-check.hidden | ||||
|       - checkbox_id = dom_id(merge_request, "selected") | ||||
|       %label.gl-sr-only{ for: checkbox_id }= merge_request.title | ||||
|       = check_box_tag checkbox_id, nil, false, 'data-id' => merge_request.id, class: "selected-issuable" | ||||
|     .issue-check.gl-mr-3.hidden | ||||
|       = render Pajamas::CheckboxTagComponent.new(name: dom_id(merge_request, "selected"), | ||||
|         value: nil, | ||||
|         checkbox_options: { 'data-id' => merge_request.id }) do |c| | ||||
|         = c.label do | ||||
|           %span.gl-sr-only | ||||
|             = merge_request.title | ||||
| 
 | ||||
|   .issuable-info-container | ||||
|     .issuable-main-info | ||||
|  |  | |||
|  | @ -7,4 +7,7 @@ | |||
|   = _("Edit Pipeline Schedule") | ||||
| %hr | ||||
| 
 | ||||
| = render "form" | ||||
| - if Feature.enabled?(:pipeline_schedules_vue, @project) | ||||
|   #pipeline-schedules-form-edit{ data: { full_path: @project.full_path } } | ||||
| - else | ||||
|   = render "form" | ||||
|  |  | |||
|  | @ -3,21 +3,25 @@ | |||
| - add_page_specific_style 'page_bundles/pipeline_schedules' | ||||
| 
 | ||||
| #pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), illustration_url: image_path('illustrations/pipeline_schedule_callout.svg') } } | ||||
| .top-area | ||||
|   - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) } | ||||
|   = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope | ||||
| 
 | ||||
|   - if can?(current_user, :create_pipeline_schedule, @project) | ||||
|     .nav-controls | ||||
|       = link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-confirm' do | ||||
|         %span= _('New schedule') | ||||
| 
 | ||||
| - if @schedules.present? | ||||
|   %ul.content-list | ||||
|     = render partial: "table" | ||||
| - if Feature.enabled?(:pipeline_schedules_vue, @project) | ||||
|   #pipeline-schedules-app{ data: { full_path: @project.full_path } } | ||||
| - else | ||||
|   = render Pajamas::CardComponent.new(card_options: { class: 'bg-light gl-mt-3 gl-text-center' }) do |c| | ||||
|     - c.body do | ||||
|       = _("No schedules") | ||||
|   .top-area | ||||
|     - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) } | ||||
|     = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope | ||||
| 
 | ||||
| #pipeline-take-ownership-modal | ||||
|     - if can?(current_user, :create_pipeline_schedule, @project) | ||||
|       .nav-controls | ||||
|         = link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-confirm' do | ||||
|           %span= _('New schedule') | ||||
| 
 | ||||
|   - if @schedules.present? | ||||
|     %ul.content-list | ||||
|       = render partial: "table" | ||||
|   - else | ||||
|     = render Pajamas::CardComponent.new(card_options: { class: 'bg-light gl-mt-3 gl-text-center' }) do |c| | ||||
|       - c.body do | ||||
|         = _("No schedules") | ||||
| 
 | ||||
|   #pipeline-take-ownership-modal | ||||
|  |  | |||
|  | @ -8,4 +8,7 @@ | |||
| %h1.page-title.gl-font-size-h-display | ||||
|   = _("Schedule a new pipeline") | ||||
| 
 | ||||
| = render "form" | ||||
| - if Feature.enabled?(:pipeline_schedules_vue, @project) | ||||
|   #pipeline-schedules-form-new{ data: { full_path: @project.full_path } } | ||||
| - else | ||||
|   = render "form" | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
|     = gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| | ||||
|       %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } | ||||
|       = render 'projects/merge_request_settings', form: f | ||||
|       = f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' } | ||||
|       = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }, pajamas_button: true | ||||
| 
 | ||||
| = render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true | ||||
| = render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true | ||||
|  |  | |||
|  | @ -11,10 +11,11 @@ | |||
|         - if params[:search].present? | ||||
|           = hidden_field_tag :search, params[:search] | ||||
|         - if @can_bulk_update | ||||
|           .check-all-holder.gl-display-none.gl-sm-display-block.hidden.gl-float-left.gl-mr-5.gl-line-height-36 | ||||
|             - checkbox_id = 'check-all-issues' | ||||
|             %label.gl-sr-only{ for: checkbox_id }= _('Select all') | ||||
|             = check_box_tag checkbox_id, nil, false, class: "check-all-issues left" | ||||
|           .check-all-holder.gl-display-none.gl-sm-display-block.hidden.gl-float-left.gl-mr-3.gl-line-height-36 | ||||
|             = render Pajamas::CheckboxTagComponent.new(name: 'check-all-issues', value: nil) do |c| | ||||
|               = c.label do | ||||
|                 %span.gl-sr-only | ||||
|                   = _('Select all') | ||||
|         .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row | ||||
|           .filtered-search-box | ||||
|             - if type != :boards | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| --- | ||||
| name: import_export_web_upload_stream | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93379 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370127 | ||||
| milestone: '15.3' | ||||
| name: pipeline_schedules_vue | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98683 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/375139 | ||||
| milestone: '15.5' | ||||
| type: development | ||||
| group: group::import | ||||
| group: group::pipeline execution | ||||
| default_enabled: false | ||||
|  | @ -520,6 +520,7 @@ Parameters: | |||
| | `password`                           | No       | Password                                                                                                                                                | | ||||
| | `private_profile`                    | No       | User's profile is private - true, false (default), or null (is converted to false)                                                                 | | ||||
| | `projects_limit`                     | No       | Limit projects each user can create                                                                                                                     | | ||||
| | `pronouns`                           | No       | Pronouns                                                                                                                                                | | ||||
| | `provider`                           | No       | External provider name                                                                                                                                  | | ||||
| | `public_email`                       | No       | Public email of the user (must be already verified)                                                                                                                            | | ||||
| | `shared_runners_minutes_limit` **(PREMIUM)** | No       | Can be set by administrators only. Maximum number of monthly CI/CD minutes for this user. Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0`.                                                                                                      | | ||||
|  |  | |||
|  | @ -153,6 +153,22 @@ to add it to our [GitLab Styles](https://gitlab.com/gitlab-org/gitlab-styles) ge | |||
| If the Cop targets rules that only apply to the main GitLab application, | ||||
| it should be added to [GitLab](https://gitlab.com/gitlab-org/gitlab) instead. | ||||
| 
 | ||||
| ### Cop grace period | ||||
| 
 | ||||
| A cop is in a "grace period" if it is enabled and has `Details: grace period` defined in its TODO YAML configuration. | ||||
| 
 | ||||
| On the default branch, all of the offenses from cops in the ["grace period"](../rake_tasks.md#run-rubocop-in-graceful-mode) will not fail the RuboCop CI job. The job will notify Slack in the `#f_rubocop` channel when offenses have been silenced in the scheduled pipeline. However, on merge request pipelines, the RuboCop job will fail. | ||||
| 
 | ||||
| A grace period can safely be lifted as soon as there are no warnings for 2 weeks in the `#f_rubocop` channel on Slack. | ||||
| 
 | ||||
| ### Enabling a new cop | ||||
| 
 | ||||
| 1. Enable the new cop in `.rubocop.yml` (if not already done via [`gitlab-styles`](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles)). | ||||
| 1. [Generate TODOs for the new cop](../rake_tasks.md#generate-initial-rubocop-todo-list). | ||||
| 1. [Set the new cop to "grace period"](#cop-grace-period). | ||||
| 1. Create an issue to fix TODOs and encourage Community contributions (via ~"good for new contributors" and/or ~"Seeking community contributions"). [See some examples](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=good%20for%20new%20contributors&label_name%5B%5D=static%20code%20analysis&first_page_size=20). | ||||
| 1. Create an issue to remove "grace period" after 2 weeks silence in `#f_rubocop` Slack channel. ([See an example](https://gitlab.com/gitlab-org/gitlab/-/issues/374903).) | ||||
| 
 | ||||
| #### RuboCop node pattern | ||||
| 
 | ||||
| When creating [node patterns](https://docs.rubocop.org/rubocop-ast/node_pattern.html) to match | ||||
|  |  | |||
|  | @ -465,10 +465,18 @@ When using code block style: | |||
| 
 | ||||
| ## Lists | ||||
| 
 | ||||
| - Always start list items with a capital letter, unless they're parameters or | ||||
|   commands that are in backticks, or similar. | ||||
| - Always leave a blank line before and after a list. | ||||
| - Begin a line with spaces (not tabs) to denote a [nested sub-item](#nesting-inside-a-list-item). | ||||
| - Use a period after every sentence, including those that complete an introductory phrase. | ||||
|   Do not use semicolons or commas. | ||||
| - Majority rules. Use either full sentences or all fragments. Avoid a mix. | ||||
| - Always start list items with a capital letter. | ||||
| - Separate the introductory phrase from explanatory text with a colon (`:`). For example: | ||||
| 
 | ||||
|   ```markdown | ||||
|   You can: | ||||
| 
 | ||||
|   - Do this thing. | ||||
|   - Do this other thing. | ||||
|   ``` | ||||
| 
 | ||||
| ### Choose between an ordered or unordered list | ||||
| 
 | ||||
|  | @ -492,39 +500,26 @@ These things are imported: | |||
| - Thing 3 | ||||
| ``` | ||||
| 
 | ||||
| You can choose to introduce either list with a colon, but you do not have to. | ||||
| 
 | ||||
| ### Markup | ||||
| ### List markup | ||||
| 
 | ||||
| - Use dashes (`-`) for unordered lists instead of asterisks (`*`). | ||||
| - Prefix `1.` to every item in an ordered list. When rendered, the list items | ||||
|   display with sequential numbering. | ||||
| 
 | ||||
| ### Punctuation | ||||
| 
 | ||||
| - Don't add commas (`,`) or semicolons (`;`) to the ends of list items. | ||||
| - If a list item is a complete sentence (with a subject and a verb), add a period at the end. | ||||
| - Majority rules. If the majority of items do not end in a period, do not end any of the items in a period. | ||||
| - Separate list items from explanatory text with a colon (`:`). For example: | ||||
| 
 | ||||
|   ```markdown | ||||
|   The list is as follows: | ||||
| 
 | ||||
|   - First item: this explains the first item. | ||||
|   - Second item: this explains the second item. | ||||
|   ``` | ||||
| - Start every item in an ordered list with `1.`. When rendered, the list items | ||||
|   are sequential. | ||||
| - Leave a blank line before and after a list. | ||||
| - Begin a line with spaces (not tabs) to denote a [nested sub-item](#nesting-inside-a-list-item). | ||||
| 
 | ||||
| ### Nesting inside a list item | ||||
| 
 | ||||
| It's possible to nest items under a list item, so that they render with the same | ||||
| indentation as the list item. This can be done with: | ||||
| You can nest items under a list item, so they render with the same | ||||
| indentation as the list item. You can do this with: | ||||
| 
 | ||||
| - [Code blocks](#code-blocks) | ||||
| - [Blockquotes](#blockquotes) | ||||
| - [Alert boxes](#alert-boxes) | ||||
| - [Images](#images) | ||||
| - [Tabs](#tabs) | ||||
| 
 | ||||
| Items nested in lists should always align with the first character of the list | ||||
| Nested items should always align with the first character of the list | ||||
| item. For unordered lists (using `-`), use two spaces for each level of | ||||
| indentation: | ||||
| 
 | ||||
|  | @ -555,26 +550,9 @@ For ordered lists, use three spaces for each level of indentation: | |||
| 1. Ordered list item 1 | ||||
| 
 | ||||
|    A line nested using 3 spaces to align with the `O` above. | ||||
| 
 | ||||
| 1. Ordered list item 2 | ||||
| 
 | ||||
|    > A quote block that will nest | ||||
|    > inside list item 2. | ||||
| 
 | ||||
| 1. Ordered list item 3 | ||||
| 
 | ||||
|    ```plaintext | ||||
|    a code block that nests inside list item 3 | ||||
|    ``` | ||||
| 
 | ||||
| 1. Ordered list item 4 | ||||
| 
 | ||||
|     | ||||
| ```` | ||||
| 
 | ||||
| You can nest full lists inside other lists using the same rules as above. If you | ||||
| want to mix types, that's also possible, if you don't mix items at the same | ||||
| level: | ||||
| You can nest lists in other lists. | ||||
| 
 | ||||
| ```markdown | ||||
| 1. Ordered list item one. | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ To enable the Microsoft Azure OAuth 2.0 OmniAuth provider, you must register | |||
| an Azure application and get a client ID and secret key. | ||||
| 
 | ||||
| 1. Sign in to the [Azure portal](https://portal.azure.com). | ||||
| 1. If you have multiple Azure Active Directory tenants, switch to the desired tenant. | ||||
| 1. If you have multiple Azure Active Directory tenants, switch to the desired tenant. Note the tenant ID. | ||||
| 1. [Register an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) | ||||
|    and provide the following information: | ||||
|    - The redirect URI, which requires the URL of the Azure OAuth callback of your GitLab | ||||
|  | @ -70,7 +70,7 @@ Alternatively, add the `User.Read.All` application permission. | |||
| 
 | ||||
| 1. [Configure the initial settings](omniauth.md#configure-initial-settings). | ||||
| 
 | ||||
| 1. Add the provider configuration. Replace `CLIENT ID`, `CLIENT SECRET`, and `TENANT ID` | ||||
| 1. Add the provider configuration. Replace `<client_id>`, `<client_secret>`, and `<tenant_id>` | ||||
|    with the values you got when you registered the Azure application. | ||||
| 
 | ||||
|    - **For Omnibus installations** | ||||
|  | @ -83,9 +83,9 @@ Alternatively, add the `User.Read.All` application permission. | |||
|          name: "azure_oauth2", | ||||
|          # label: "Provider name", # optional label for login button, defaults to "Azure AD" | ||||
|          args: { | ||||
|            client_id: "CLIENT ID", | ||||
|            client_secret: "CLIENT SECRET", | ||||
|            tenant_id: "TENANT ID", | ||||
|            client_id: "<client_id>", | ||||
|            client_secret: "<client_secret>", | ||||
|            tenant_id: "<tenant_id>", | ||||
|          } | ||||
|        } | ||||
|      ] | ||||
|  | @ -99,9 +99,9 @@ Alternatively, add the `User.Read.All` application permission. | |||
|          "name" => "azure_activedirectory_v2", | ||||
|          "label" => "Provider name", # optional label for login button, defaults to "Azure AD v2" | ||||
|          "args" => { | ||||
|            "client_id" => "CLIENT ID", | ||||
|            "client_secret" => "CLIENT SECRET", | ||||
|            "tenant_id" => "TENANT ID", | ||||
|            "client_id" => "<client_id>", | ||||
|            "client_secret" => "<client_secret>", | ||||
|            "tenant_id" => "<tenant_id>", | ||||
|          } | ||||
|        } | ||||
|      ] | ||||
|  | @ -116,9 +116,9 @@ Alternatively, add the `User.Read.All` application permission. | |||
|          "name" => "azure_activedirectory_v2", | ||||
|          "label" => "Provider name", # optional label for login button, defaults to "Azure AD v2" | ||||
|          "args" => { | ||||
|            "client_id" => "CLIENT ID", | ||||
|            "client_secret" => "CLIENT SECRET", | ||||
|            "tenant_id" => "TENANT ID", | ||||
|            "client_id" => "<client_id>", | ||||
|            "client_secret" => "<client_secret>", | ||||
|            "tenant_id" => "<tenant_id>", | ||||
|            "base_azure_url" => "https://login.microsoftonline.us" | ||||
|          } | ||||
|        } | ||||
|  | @ -132,9 +132,9 @@ Alternatively, add the `User.Read.All` application permission. | |||
|      ```yaml | ||||
|      - { name: 'azure_oauth2', | ||||
|          # label: 'Provider name', # optional label for login button, defaults to "Azure AD" | ||||
|          args: { client_id: 'CLIENT ID', | ||||
|                  client_secret: 'CLIENT SECRET', | ||||
|                  tenant_id: 'TENANT ID' } } | ||||
|          args: { client_id: '<client_id>', | ||||
|                  client_secret: '<client_secret>', | ||||
|                  tenant_id: '<tenant_id>' } } | ||||
|      ``` | ||||
| 
 | ||||
|      For the v2.0 endpoint: | ||||
|  | @ -142,9 +142,9 @@ Alternatively, add the `User.Read.All` application permission. | |||
|      ```yaml | ||||
|      - { name: 'azure_activedirectory_v2', | ||||
|          label: 'Provider name', # optional label for login button, defaults to "Azure AD v2" | ||||
|          args: { client_id: "CLIENT ID", | ||||
|                  client_secret: "CLIENT SECRET", | ||||
|                  tenant_id: "TENANT ID" } } | ||||
|          args: { client_id: "<client_id>", | ||||
|                  client_secret: "<client_secret>", | ||||
|                  tenant_id: "<tenant_id>" } } | ||||
|      ``` | ||||
| 
 | ||||
|      For [alternative Azure clouds](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud), | ||||
|  | @ -153,15 +153,13 @@ Alternatively, add the `User.Read.All` application permission. | |||
|      ```yaml | ||||
|      - { name: 'azure_activedirectory_v2', | ||||
|          label: 'Provider name', # optional label for login button, defaults to "Azure AD v2" | ||||
|          args: { client_id: "CLIENT ID", | ||||
|                  client_secret: "CLIENT SECRET", | ||||
|                  tenant_id: "TENANT ID", | ||||
|          args: { client_id: "<client_id>", | ||||
|                  client_secret: "<client_secret>", | ||||
|                  tenant_id: "<tenant_id>", | ||||
|                  base_azure_url: "https://login.microsoftonline.us" } } | ||||
|      ``` | ||||
| 
 | ||||
|    In addition, you can optionally add the following parameters to the `args` section: | ||||
| 
 | ||||
|    - `scope` for [OAuth2 scopes](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow). The default is `openid profile email`. | ||||
|    You can also optionally add the `scope` for [OAuth 2.0 scopes](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) parameter to the `args` section. The default is `openid profile email`. | ||||
| 
 | ||||
| 1. Save the configuration file. | ||||
| 
 | ||||
|  |  | |||
|  | @ -193,3 +193,14 @@ The modules that can be configured for logging are as follows: | |||
| | `NAVDB`    | Used for persistence mechanisms to store navigation entries. | | ||||
| | `REPT`     | Used for generating reports. | | ||||
| | `STAT`     | Used for general statistics while running the scan. | | ||||
| 
 | ||||
| ### Artifacts | ||||
| 
 | ||||
| DAST's browser-based analyzer generates artifacts that can help you understand how the scanner works.  | ||||
| Using the latest version of the DAST [template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml) these artifacts are exposed for download by default. | ||||
| 
 | ||||
| The list of artifacts includes the following files: | ||||
| 
 | ||||
| - `gl-dast-debug-auth-report.html` | ||||
| - `gl-dast-debug-crawl-report.html` | ||||
| - `gl-dast-crawl-graph.svg` | ||||
|  |  | |||
|  | @ -194,6 +194,15 @@ References to pull requests and issues are preserved. Each imported repository m | |||
| [visibility level is restricted](../../public_access.md#restrict-use-of-public-or-internal-projects), in which case it | ||||
| defaults to the default project visibility. | ||||
| 
 | ||||
| ### Branch protection rules | ||||
| 
 | ||||
| Supported GitHub branch protection rules are mapped to GitLab branch protection rules or project-wide GitLab settings when they are imported: | ||||
| 
 | ||||
| - GitHub rule **Require conversation resolution before merging** for the project's default branch is mapped to the [**All threads must be resolved** GitLab setting](../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371110) in GitLab 15.5. | ||||
| - Support for GitHub rule **Require a pull request before merging** is proposed in issue [370951](https://gitlab.com/gitlab-org/gitlab/-/issues/370951). | ||||
| - Support for GitHub rule **Require signed commits** is proposed in issue [370949](https://gitlab.com/gitlab-org/gitlab/-/issues/370949). | ||||
| - Support for GitHub rule **Require status checks to pass before merging** is proposed in issue [370948](https://gitlab.com/gitlab-org/gitlab/-/issues/370948). | ||||
| 
 | ||||
| ## Alternative way to import notes and diff notes | ||||
| 
 | ||||
| When GitHub Importer runs on extremely large projects not all notes & diff notes can be imported due to GitHub API `issues_comments` & `pull_requests_comments` endpoints limitation. | ||||
|  |  | |||
|  | @ -50,6 +50,7 @@ module API | |||
|           optional :provider, type: String, desc: 'The external provider' | ||||
|           optional :bio, type: String, desc: 'The biography of the user' | ||||
|           optional :location, type: String, desc: 'The location of the user' | ||||
|           optional :pronouns, type: String, desc: 'The pronouns of the user' | ||||
|           optional :public_email, type: String, desc: 'The public email of the user' | ||||
|           optional :commit_email, type: String, desc: 'The commit email, _private for private commit email' | ||||
|           optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ code_quality: | |||
|   variables: | ||||
|     DOCKER_DRIVER: overlay2 | ||||
|     DOCKER_TLS_CERTDIR: "" | ||||
|     CODE_QUALITY_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/ci-cd/codequality:0.85.29" | ||||
|     CODE_QUALITY_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/ci-cd/codequality:0.87.0" | ||||
|   needs: [] | ||||
|   script: | ||||
|     - export SOURCE_CODE=$PWD | ||||
|  |  | |||
|  | @ -22,6 +22,8 @@ module Gitlab | |||
|           ProtectedBranches::CreateService | ||||
|             .new(project, project.creator, params) | ||||
|             .execute(skip_authorization: true) | ||||
| 
 | ||||
|           update_project_settings if default_branch? | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
|  | @ -42,6 +44,20 @@ module Gitlab | |||
|             protected_branch.allow_force_pushes | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def default_branch? | ||||
|           protected_branch.id == project.default_branch | ||||
|         end | ||||
| 
 | ||||
|         def update_project_settings | ||||
|           update_setting_for_only_allow_merge_if_all_discussions_are_resolved | ||||
|         end | ||||
| 
 | ||||
|         def update_setting_for_only_allow_merge_if_all_discussions_are_resolved | ||||
|           return unless protected_branch.required_conversation_resolution | ||||
| 
 | ||||
|           project.update(only_allow_merge_if_all_discussions_are_resolved: true) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ module Gitlab | |||
| 
 | ||||
|         attr_reader :attributes | ||||
| 
 | ||||
|         expose_attribute :id, :allow_force_pushes | ||||
|         expose_attribute :id, :allow_force_pushes, :required_conversation_resolution | ||||
| 
 | ||||
|         # Builds a Branch Protection info from a GitHub API response. | ||||
|         # Resource structure details: | ||||
|  | @ -20,7 +20,8 @@ module Gitlab | |||
| 
 | ||||
|           hash = { | ||||
|             id: branch_name, | ||||
|             allow_force_pushes: branch_protection.allow_force_pushes.enabled | ||||
|             allow_force_pushes: branch_protection.allow_force_pushes.enabled, | ||||
|             required_conversation_resolution: branch_protection.required_conversation_resolution.enabled | ||||
|           } | ||||
| 
 | ||||
|           new(hash) | ||||
|  |  | |||
|  | @ -26,10 +26,10 @@ module Gitlab | |||
|           log_info(message: "Started uploading project", export_size: export_size) | ||||
| 
 | ||||
|           upload_duration = Benchmark.realtime do | ||||
|             if Feature.enabled?(:import_export_web_upload_stream) && !project.export_file.file_storage? | ||||
|               upload_project_as_remote_stream | ||||
|             else | ||||
|             if project.export_file.file_storage? | ||||
|               handle_response_error(send_file) | ||||
|             else | ||||
|               upload_project_as_remote_stream | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ module Gitlab | |||
|     module Group | ||||
|       module Settings | ||||
|         class UsageQuotas < Chemlab::Page | ||||
|           # TODO: Supplant with data-qa-selectors | ||||
|           link :pipelines_tab | ||||
|           link :storage_tab | ||||
|           link :buy_ci_minutes | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ module QA | |||
|         Page::Group::Menu.perform(&:go_to_usage_quotas) | ||||
|         Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quota| | ||||
|           usage_quota.storage_tab | ||||
|           usage_quota.buy_storage | ||||
|           usage_quota.purchase_more_storage | ||||
|         end | ||||
| 
 | ||||
|         # Purchase checkout opens a new tab | ||||
|  |  | |||
|  | @ -11,6 +11,10 @@ RSpec.describe 'Pipeline Schedules', :js do | |||
|   let(:scope) { nil } | ||||
|   let!(:user) { create(:user) } | ||||
| 
 | ||||
|   before do | ||||
|     stub_feature_flags(pipeline_schedules_vue: false) | ||||
|   end | ||||
| 
 | ||||
|   context 'logged in as the pipeline schedule owner' do | ||||
|     before do | ||||
|       project.add_developer(user) | ||||
|  |  | |||
|  | @ -735,6 +735,8 @@ RSpec.describe 'Pipeline', :js do | |||
|         end | ||||
| 
 | ||||
|         it 'displays the PipelineSchedule in an inactive state' do | ||||
|           stub_feature_flags(pipeline_schedules_vue: false) | ||||
| 
 | ||||
|           visit project_pipeline_schedules_path(project) | ||||
|           page.click_link('Inactive') | ||||
| 
 | ||||
|  |  | |||
|  | @ -860,6 +860,8 @@ RSpec.describe 'Pipeline', :js do | |||
|         end | ||||
| 
 | ||||
|         it 'displays the PipelineSchedule in an inactive state' do | ||||
|           stub_feature_flags(pipeline_schedules_vue: false) | ||||
| 
 | ||||
|           visit project_pipeline_schedules_path(project) | ||||
|           page.click_link('Inactive') | ||||
| 
 | ||||
|  |  | |||
|  | @ -496,20 +496,15 @@ RSpec.describe MergeRequestsFinder do | |||
|       context 'filtering by approved by username' do | ||||
|         let(:params) { { approved_by_usernames: user2.username } } | ||||
| 
 | ||||
|         where(:sort) { [nil] + %w(milestone merged_at merged_at_desc closed_at closed_at_desc) } | ||||
| 
 | ||||
|         before do | ||||
|           create(:approval, merge_request: merge_request3, user: user2) | ||||
|         end | ||||
| 
 | ||||
|         it 'returns merge requests approved by that user' do | ||||
|           merge_requests = described_class.new(user, params).execute | ||||
| 
 | ||||
|           expect(merge_requests).to contain_exactly(merge_request3) | ||||
|         end | ||||
| 
 | ||||
|         context 'with sorting by milestone' do | ||||
|           let(:params) { { approved_by_usernames: user2.username, sort: 'milestone' } } | ||||
| 
 | ||||
|         with_them do | ||||
|           it 'returns merge requests approved by that user' do | ||||
|             params = { approved_by_usernames: user2.username, sort: sort } | ||||
|             merge_requests = described_class.new(user, params).execute | ||||
| 
 | ||||
|             expect(merge_requests).to contain_exactly(merge_request3) | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
|     "is_admin", | ||||
|     "bio", | ||||
|     "location", | ||||
|     "pronouns", | ||||
|     "skype", | ||||
|     "linkedin", | ||||
|     "twitter", | ||||
|  |  | |||
|  | @ -0,0 +1,25 @@ | |||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { GlForm } from '@gitlab/ui'; | ||||
| import PipelineSchedulesForm from '~/pipeline_schedules/components/pipeline_schedules_form.vue'; | ||||
| 
 | ||||
| describe('Pipeline schedules form', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = () => { | ||||
|     wrapper = shallowMount(PipelineSchedulesForm); | ||||
|   }; | ||||
| 
 | ||||
|   const findForm = () => wrapper.findComponent(GlForm); | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     createComponent(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|   }); | ||||
| 
 | ||||
|   it('displays form', () => { | ||||
|     expect(findForm().exists()).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,25 @@ | |||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import PipelineSchedules from '~/pipeline_schedules/components/pipeline_schedules.vue'; | ||||
| import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue'; | ||||
| 
 | ||||
| describe('Pipeline schedules app', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = () => { | ||||
|     wrapper = shallowMount(PipelineSchedules); | ||||
|   }; | ||||
| 
 | ||||
|   const findTable = () => wrapper.findComponent(PipelineSchedulesTable); | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     createComponent(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|   }); | ||||
| 
 | ||||
|   it('displays table', () => { | ||||
|     expect(findTable().exists()).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  | @ -0,0 +1,25 @@ | |||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { GlTableLite } from '@gitlab/ui'; | ||||
| import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue'; | ||||
| 
 | ||||
| describe('Pipeline schedules table', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = () => { | ||||
|     wrapper = shallowMount(PipelineSchedulesTable); | ||||
|   }; | ||||
| 
 | ||||
|   const findTable = () => wrapper.findComponent(GlTableLite); | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     createComponent(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|   }); | ||||
| 
 | ||||
|   it('displays table', () => { | ||||
|     expect(findTable().exists()).toBe(true); | ||||
|   }); | ||||
| }); | ||||
|  | @ -14,6 +14,7 @@ describe('Deploy freeze timezone dropdown', () => { | |||
|       propsData: { | ||||
|         value: selectedTimezone, | ||||
|         timezoneData: timezoneDataFixture, | ||||
|         name: 'user[timezone]', | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|  | @ -24,8 +25,8 @@ describe('Deploy freeze timezone dropdown', () => { | |||
| 
 | ||||
|   const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); | ||||
|   const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); | ||||
|   const findDropdown = () => wrapper.findComponent(GlDropdown); | ||||
|   const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults'); | ||||
|   const findHiddenInput = () => wrapper.find('input'); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|  | @ -84,13 +85,27 @@ describe('Deploy freeze timezone dropdown', () => { | |||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Selected time zone', () => { | ||||
|   describe('Selected time zone not found', () => { | ||||
|     beforeEach(() => { | ||||
|       createComponent('', 'Alaska'); | ||||
|       createComponent('', 'Berlin'); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders empty selections', () => { | ||||
|       expect(wrapper.findComponent(GlDropdown).props().text).toBe('Select timezone'); | ||||
|     }); | ||||
| 
 | ||||
|     it('preserves initial value in the associated input', () => { | ||||
|       expect(findHiddenInput().attributes('value')).toBe('Berlin'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Selected time zone found', () => { | ||||
|     beforeEach(() => { | ||||
|       createComponent('', 'Europe/Berlin'); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders selected time zone as dropdown label', () => { | ||||
|       expect(findDropdown().vm.text).toBe('Alaska'); | ||||
|       expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC + 2] Berlin'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -486,6 +486,25 @@ RSpec.describe ApplicationHelper do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#gitlab_ui_form_with' do | ||||
|     let_it_be(:user) { build(:user) } | ||||
| 
 | ||||
|     before do | ||||
|       allow(helper).to receive(:users_path).and_return('/root') | ||||
|       allow(helper).to receive(:form_with).and_call_original | ||||
|     end | ||||
| 
 | ||||
|     it 'adds custom form builder to options and calls `form_with`' do | ||||
|       options = { model: user, html: { class: 'foo-bar' } } | ||||
|       expected_options = options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder }) | ||||
| 
 | ||||
|       expect do |b| | ||||
|         helper.gitlab_ui_form_with(**options, &b) | ||||
|       end.to yield_with_args(::Gitlab::FormBuilders::GitlabUiFormBuilder) | ||||
|       expect(helper).to have_received(:form_with).with(expected_options) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#page_class' do | ||||
|     context 'when logged_out_marketing_header experiment is enabled' do | ||||
|       let_it_be(:expected_class) { 'logged-out-marketing-header-candidate' } | ||||
|  |  | |||
|  | @ -5,11 +5,14 @@ require 'spec_helper' | |||
| RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do | ||||
|   subject(:importer) { described_class.new(github_protected_branch, project, client) } | ||||
| 
 | ||||
|   let(:branch_name) { 'protection' } | ||||
|   let(:allow_force_pushes_on_github) { true } | ||||
|   let(:required_conversation_resolution) { true } | ||||
|   let(:github_protected_branch) do | ||||
|     Gitlab::GithubImport::Representation::ProtectedBranch.new( | ||||
|       id: 'protection', | ||||
|       allow_force_pushes: allow_force_pushes_on_github | ||||
|       id: branch_name, | ||||
|       allow_force_pushes: allow_force_pushes_on_github, | ||||
|       required_conversation_resolution: required_conversation_resolution | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|  | @ -47,6 +50,12 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples 'does not change project attributes' do | ||||
|       it 'does not change only_allow_merge_if_all_discussions_are_resolved' do | ||||
|         expect { importer.execute }.not_to change(project, :only_allow_merge_if_all_discussions_are_resolved) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when branch is protected on GitLab' do | ||||
|       before do | ||||
|         create( | ||||
|  | @ -87,5 +96,39 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do | |||
| 
 | ||||
|       it_behaves_like 'create branch protection by the strictest ruleset' | ||||
|     end | ||||
| 
 | ||||
|     context "when branch is default" do | ||||
|       before do | ||||
|         allow(project).to receive(:default_branch).and_return(branch_name) | ||||
|       end | ||||
| 
 | ||||
|       context 'when required_conversation_resolution rule is enabled' do | ||||
|         let(:required_conversation_resolution) { true } | ||||
| 
 | ||||
|         it 'changes project settings' do | ||||
|           expect { importer.execute }.to change(project, :only_allow_merge_if_all_discussions_are_resolved).to(true) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when required_conversation_resolution rule is disabled' do | ||||
|         let(:required_conversation_resolution) { false } | ||||
| 
 | ||||
|         it_behaves_like 'does not change project attributes' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when branch is not default" do | ||||
|       context 'when required_conversation_resolution rule is enabled' do | ||||
|         let(:required_conversation_resolution) { true } | ||||
| 
 | ||||
|         it_behaves_like 'does not change project attributes' | ||||
|       end | ||||
| 
 | ||||
|       context 'when required_conversation_resolution rule is disabled' do | ||||
|         let(:required_conversation_resolution) { false } | ||||
| 
 | ||||
|         it_behaves_like 'does not change project attributes' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -9,23 +9,30 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do | |||
|     end | ||||
| 
 | ||||
|     context 'with ProtectedBranch' do | ||||
|       it 'includes the protected branch ID (name)' do | ||||
|       it 'includes the protected branch ID (name) attribute' do | ||||
|         expect(protected_branch.id).to eq 'main' | ||||
|       end | ||||
| 
 | ||||
|       it 'includes the protected branch allow_force_pushes' do | ||||
|       it 'includes the protected branch allow_force_pushes attribute' do | ||||
|         expect(protected_branch.allow_force_pushes).to eq true | ||||
|       end | ||||
| 
 | ||||
|       it 'includes the protected branch required_conversation_resolution attribute' do | ||||
|         expect(protected_branch.required_conversation_resolution).to eq true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.from_api_response' do | ||||
|     let(:response) do | ||||
|       response = Struct.new(:url, :allow_force_pushes, keyword_init: true) | ||||
|       allow_force_pushes = Struct.new(:enabled, keyword_init: true) | ||||
|       response = Struct.new(:url, :allow_force_pushes, :required_conversation_resolution, keyword_init: true) | ||||
|       enabled_setting = Struct.new(:enabled, keyword_init: true) | ||||
|       response.new( | ||||
|         url: 'https://example.com/branches/main/protection', | ||||
|         allow_force_pushes: allow_force_pushes.new( | ||||
|         allow_force_pushes: enabled_setting.new( | ||||
|           enabled: true | ||||
|         ), | ||||
|         required_conversation_resolution: enabled_setting.new( | ||||
|           enabled: true | ||||
|         ) | ||||
|       ) | ||||
|  | @ -41,7 +48,8 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do | |||
|       let(:hash) do | ||||
|         { | ||||
|           'id' => 'main', | ||||
|           'allow_force_pushes' => true | ||||
|           'allow_force_pushes' => true, | ||||
|           'required_conversation_resolution' => true | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,8 +9,6 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do | |||
|     allow_next_instance_of(ProjectExportWorker) do |job| | ||||
|       allow(job).to receive(:jid).and_return(SecureRandom.hex(8)) | ||||
|     end | ||||
| 
 | ||||
|     stub_feature_flags(import_export_web_upload_stream: false) | ||||
|     stub_uploads_object_storage(FileUploader, enabled: false) | ||||
|   end | ||||
| 
 | ||||
|  | @ -109,108 +107,68 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do | |||
|     end | ||||
| 
 | ||||
|     context 'when object store is enabled' do | ||||
|       let(:object_store_url) { 'http://object-storage/project.tar.gz' } | ||||
| 
 | ||||
|       before do | ||||
|         object_store_url = 'http://object-storage/project.tar.gz' | ||||
|         stub_uploads_object_storage(FileUploader) | ||||
|         stub_request(:get, object_store_url) | ||||
|         stub_request(:post, example_url) | ||||
| 
 | ||||
|         allow(import_export_upload.export_file).to receive(:url).and_return(object_store_url) | ||||
|         allow(import_export_upload.export_file).to receive(:file_storage?).and_return(false) | ||||
|       end | ||||
| 
 | ||||
|       it 'reads file using Gitlab::HttpIO and uploads to external url' do | ||||
|         expect_next_instance_of(Gitlab::HttpIO) do |http_io| | ||||
|           expect(http_io).to receive(:read).and_call_original | ||||
|       it 'uploads file as a remote stream' do | ||||
|         arguments = { | ||||
|           download_url: object_store_url, | ||||
|           upload_url: example_url, | ||||
|           options: { | ||||
|             upload_method: :post, | ||||
|             upload_content_type: 'application/gzip' | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         expect_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload, arguments) do |remote_stream_upload| | ||||
|           expect(remote_stream_upload).to receive(:execute) | ||||
|         end | ||||
|         expect(Gitlab::ImportExport::RemoteStreamUpload).not_to receive(:new) | ||||
|         expect(Gitlab::HttpIO).not_to receive(:new) | ||||
| 
 | ||||
|         strategy.execute(user, project) | ||||
| 
 | ||||
|         expect(a_request(:post, example_url)).to have_been_made | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when `import_export_web_upload_stream` feature is enabled' do | ||||
|       before do | ||||
|         stub_feature_flags(import_export_web_upload_stream: true) | ||||
|       end | ||||
| 
 | ||||
|       context 'when remote object store is disabled' do | ||||
|         it 'reads file from disk and uploads to external url' do | ||||
|           stub_request(:post, example_url).to_return(status: 200) | ||||
|           expect(Gitlab::ImportExport::RemoteStreamUpload).not_to receive(:new) | ||||
|           expect(Gitlab::HttpIO).not_to receive(:new) | ||||
| 
 | ||||
|           strategy.execute(user, project) | ||||
| 
 | ||||
|           expect(a_request(:post, example_url)).to have_been_made | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when object store is enabled' do | ||||
|         let(:object_store_url) { 'http://object-storage/project.tar.gz' } | ||||
| 
 | ||||
|       context 'when upload as remote stream raises an exception' do | ||||
|         before do | ||||
|           stub_uploads_object_storage(FileUploader) | ||||
| 
 | ||||
|           allow(import_export_upload.export_file).to receive(:url).and_return(object_store_url) | ||||
|           allow(import_export_upload.export_file).to receive(:file_storage?).and_return(false) | ||||
|           allow_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload) do |remote_stream_upload| | ||||
|             allow(remote_stream_upload).to receive(:execute).and_raise( | ||||
|               Gitlab::ImportExport::RemoteStreamUpload::StreamError.new('Exception error message', 'Response body') | ||||
|             ) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         it 'uploads file as a remote stream' do | ||||
|           arguments = { | ||||
|             download_url: object_store_url, | ||||
|             upload_url: example_url, | ||||
|             options: { | ||||
|               upload_method: :post, | ||||
|               upload_content_type: 'application/gzip' | ||||
|             } | ||||
|           } | ||||
|         it 'logs the exception and stores the error message' do | ||||
|           expect_next_instance_of(Gitlab::Export::Logger) do |logger| | ||||
|             expect(logger).to receive(:error).ordered.with( | ||||
|               { | ||||
|                 project_id: project.id, | ||||
|                 project_name: project.name, | ||||
|                 message: 'Exception error message', | ||||
|                 response_body: 'Response body' | ||||
|               } | ||||
|             ) | ||||
| 
 | ||||
|           expect_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload, arguments) do |remote_stream_upload| | ||||
|             expect(remote_stream_upload).to receive(:execute) | ||||
|             expect(logger).to receive(:error).ordered.with( | ||||
|               { | ||||
|                 project_id: project.id, | ||||
|                 project_name: project.name, | ||||
|                 message: 'After export strategy failed', | ||||
|                 'exception.class' => 'Gitlab::ImportExport::RemoteStreamUpload::StreamError', | ||||
|                 'exception.message' => 'Exception error message', | ||||
|                 'exception.backtrace' => anything | ||||
|               } | ||||
|             ) | ||||
|           end | ||||
|           expect(Gitlab::HttpIO).not_to receive(:new) | ||||
| 
 | ||||
|           strategy.execute(user, project) | ||||
|         end | ||||
| 
 | ||||
|         context 'when upload as remote stream raises an exception' do | ||||
|           before do | ||||
|             allow_next_instance_of(Gitlab::ImportExport::RemoteStreamUpload) do |remote_stream_upload| | ||||
|               allow(remote_stream_upload).to receive(:execute).and_raise( | ||||
|                 Gitlab::ImportExport::RemoteStreamUpload::StreamError.new('Exception error message', 'Response body') | ||||
|               ) | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|           it 'logs the exception and stores the error message' do | ||||
|             expect_next_instance_of(Gitlab::Export::Logger) do |logger| | ||||
|               expect(logger).to receive(:error).ordered.with( | ||||
|                 { | ||||
|                   project_id: project.id, | ||||
|                   project_name: project.name, | ||||
|                   message: 'Exception error message', | ||||
|                   response_body: 'Response body' | ||||
|                 } | ||||
|               ) | ||||
| 
 | ||||
|               expect(logger).to receive(:error).ordered.with( | ||||
|                 { | ||||
|                   project_id: project.id, | ||||
|                   project_name: project.name, | ||||
|                   message: 'After export strategy failed', | ||||
|                   'exception.class' => 'Gitlab::ImportExport::RemoteStreamUpload::StreamError', | ||||
|                   'exception.message' => 'Exception error message', | ||||
|                   'exception.backtrace' => anything | ||||
|                 } | ||||
|               ) | ||||
|             end | ||||
| 
 | ||||
|             strategy.execute(user, project) | ||||
| 
 | ||||
|             expect(project.import_export_shared.errors.first).to eq('Exception error message') | ||||
|           end | ||||
|           expect(project.import_export_shared.errors.first).to eq('Exception error message') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue