Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-28 18:08:30 +00:00
parent 1bb7f81e23
commit 7f3f19582b
50 changed files with 560 additions and 245 deletions

View File

@ -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) {

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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>

View File

@ -0,0 +1,18 @@
<script>
import { GlForm } from '@gitlab/ui';
export default {
components: {
GlForm,
},
inject: {
fullPath: {
default: '',
},
},
};
</script>
<template>
<gl-form />
</template>

View File

@ -0,0 +1,13 @@
<script>
import { GlTableLite } from '@gitlab/ui';
export default {
components: {
GlTableLite,
},
};
</script>
<template>
<gl-table-lite />
</template>

View File

@ -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);
},
});
};

View File

@ -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);
},
});
};

View File

@ -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

View File

@ -735,13 +735,7 @@
}
.issue-check {
padding-right: $gl-padding;
margin-bottom: 10px;
min-width: 15px;
.selected-issuable {
vertical-align: text-top;
}
}
.issuable-milestone,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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" }

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`. |

View File

@ -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

View File

@ -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.

View File

@ -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.

View 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`

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -13,6 +13,7 @@
"is_admin",
"bio",
"location",
"pronouns",
"skype",
"linkedin",
"twitter",

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});
});

View File

@ -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' }

View File

@ -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

View File

@ -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

View File

@ -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