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
|
||||
|
||||
![an image that will nest inside list item 4](image.png)
|
||||
````
|
||||
|
||||
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