-
+
issue.id, class: "selected-issuable"
.issuable-main-info
.issue-title.title
%span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 98d2928fc97..71f8e4c32f5 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -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
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 642b458eea6..3f843ce6aec 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -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"
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index a0412650b3a..6c7b4b2f320 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -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
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index 3b4acf5b8c5..d3757d0e339 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -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"
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index 279a5a3b0f5..b7adba3ec39 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -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
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 21716710015..72940b64801 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -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
diff --git a/config/feature_flags/development/import_export_web_upload_stream.yml b/config/feature_flags/development/pipeline_schedules_vue.yml
similarity index 62%
rename from config/feature_flags/development/import_export_web_upload_stream.yml
rename to config/feature_flags/development/pipeline_schedules_vue.yml
index 59e06fbec43..e497d0f9c4e 100644
--- a/config/feature_flags/development/import_export_web_upload_stream.yml
+++ b/config/feature_flags/development/pipeline_schedules_vue.yml
@@ -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
diff --git a/doc/api/users.md b/doc/api/users.md
index 5933316625c..333998dffb2 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -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`. |
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index 8fd5a21260a..4ef3fa77958 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -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
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 92516a4b7c4..c7d75ce5ace 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -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.
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 881338b8569..15f8e7d01e5 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -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 ``, ``, and ``
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_secret: "",
+ 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_secret" => "",
+ "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_secret" => "",
+ "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_secret: '',
+ 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_secret: "",
+ 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_secret: "",
+ 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.
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index d72d44fdf48..64f36e24cad 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -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`
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index a840fd3c293..818cd7e1b74 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -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.
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 7acdd0c6325..3a2bbc3fb32 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -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'
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index e278539d214..fa2d641ba95 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -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
diff --git a/lib/gitlab/github_import/importer/protected_branch_importer.rb b/lib/gitlab/github_import/importer/protected_branch_importer.rb
index 16215fdce8e..aba51e28505 100644
--- a/lib/gitlab/github_import/importer/protected_branch_importer.rb
+++ b/lib/gitlab/github_import/importer/protected_branch_importer.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/protected_branch.rb b/lib/gitlab/github_import/representation/protected_branch.rb
index b80b7cf1076..ab8909d955c 100644
--- a/lib/gitlab/github_import/representation/protected_branch.rb
+++ b/lib/gitlab/github_import/representation/protected_branch.rb
@@ -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)
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index 6c5fba37d7b..fe0ab01e4fd 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -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
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
index 9f34f48fee0..596f9cd7748 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
@@ -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
diff --git a/qa/qa/flow/purchase.rb b/qa/qa/flow/purchase.rb
index c07e03c104d..a77b55c7974 100644
--- a/qa/qa/flow/purchase.rb
+++ b/qa/qa/flow/purchase.rb
@@ -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
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index dcc46f5d223..4ed0a11da38 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -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)
diff --git a/spec/features/projects/pipelines/legacy_pipeline_spec.rb b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
index 250a336469c..d93c951791d 100644
--- a/spec/features/projects/pipelines/legacy_pipeline_spec.rb
+++ b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
@@ -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')
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 51a6fbc4d36..0b43e13996f 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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')
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index deeca6132e0..54081e21a46 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -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)
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/admin.json b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
index dce3951a4d2..f0d3cf3ba0e 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/admin.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/admin.json
@@ -13,6 +13,7 @@
"is_admin",
"bio",
"location",
+ "pronouns",
"skype",
"linkedin",
"twitter",
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
new file mode 100644
index 00000000000..4b5a9611251
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
new file mode 100644
index 00000000000..98e53942d19
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
new file mode 100644
index 00000000000..950b5d64ffe
--- /dev/null
+++ b/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
index 48f811c8135..e5f56c63031 100644
--- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -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');
});
});
});
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 264431b1bb5..a4b2c963c74 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -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' }
diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
index 6dc6db739f4..12d143f3692 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
index e762dc469c1..1d8426c0ad8 100644
--- a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 42cf9c54798..297fe3ade07 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -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