Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3a52eefc27
commit
d1ade10ba6
|
@ -84,6 +84,7 @@ review-qa-reliable:
|
|||
- .review-qa-base
|
||||
- .review:rules:review-qa-reliable
|
||||
- .parallel-qa-base
|
||||
retry: 1
|
||||
variables:
|
||||
QA_RUN_TYPE: review-qa-reliable
|
||||
QA_SCENARIO: Test::Instance::Reliable
|
||||
|
|
|
@ -1 +1 @@
|
|||
a191a5d10f0772ae2ed6ec869001ddde6d277827
|
||||
2247949bf06c7e8b28ef629dccb347868ee3abf4
|
||||
|
|
|
@ -49,6 +49,7 @@ fragment EpicNode on Epic {
|
|||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
color
|
||||
description
|
||||
textColor
|
||||
|
@ -140,6 +141,7 @@ query childItems(
|
|||
__typename
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
color
|
||||
description
|
||||
textColor
|
||||
|
|
|
@ -576,18 +576,12 @@ class Project < ApplicationRecord
|
|||
.where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
|
||||
end
|
||||
|
||||
# "enabled" here means "not disabled". It includes private features!
|
||||
scope :with_feature_enabled, ->(feature) {
|
||||
access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
|
||||
enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
|
||||
|
||||
with_project_feature.where(enabled_feature)
|
||||
with_project_feature.merge(ProjectFeature.with_feature_enabled(feature))
|
||||
}
|
||||
|
||||
# Picks a feature where the level is exactly that given.
|
||||
scope :with_feature_access_level, ->(feature, level) {
|
||||
access_level_attribute = ProjectFeature.access_level_attribute(feature)
|
||||
with_project_feature.where(project_features: { access_level_attribute => level })
|
||||
with_project_feature.merge(ProjectFeature.with_feature_access_level(feature, level))
|
||||
}
|
||||
|
||||
# Picks projects which use the given programming language
|
||||
|
@ -688,37 +682,8 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
|
||||
# they are only available to team members. This scope returns projects where
|
||||
# the feature is either public, enabled, or internal with permission for the user.
|
||||
# Note: this scope doesn't enforce that the user has access to the projects, it just checks
|
||||
# that the user has access to the feature. It's important to use this scope with others
|
||||
# that checks project authorizations first (e.g. `filter_by_feature_visibility`).
|
||||
#
|
||||
# This method uses an optimised version of `with_feature_access_level` for
|
||||
# logged in users to more efficiently get private projects with the given
|
||||
# feature.
|
||||
def self.with_feature_available_for_user(feature, user)
|
||||
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
|
||||
|
||||
if user&.can_read_all_resources?
|
||||
with_feature_enabled(feature)
|
||||
elsif user
|
||||
min_access_level = ProjectFeature.required_minimum_access_level(feature)
|
||||
column = ProjectFeature.quoted_access_level_column(feature)
|
||||
|
||||
with_project_feature
|
||||
.where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
|
||||
{
|
||||
public_visible: visible,
|
||||
private_visible: ProjectFeature::PRIVATE,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
|
||||
})
|
||||
else
|
||||
# This has to be added to include features whose value is nil in the db
|
||||
visible << nil
|
||||
with_feature_access_level(feature, visible)
|
||||
end
|
||||
with_project_feature.merge(ProjectFeature.with_feature_available_for_user(feature, user))
|
||||
end
|
||||
|
||||
def self.projects_user_can(projects, user, action)
|
||||
|
|
|
@ -83,6 +83,52 @@ class ProjectFeature < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# "enabled" here means "not disabled". It includes private features!
|
||||
scope :with_feature_enabled, ->(feature) {
|
||||
feature_access_level_attribute = arel_table[access_level_attribute(feature)]
|
||||
enabled_feature = feature_access_level_attribute.gt(DISABLED).or(feature_access_level_attribute.eq(nil))
|
||||
|
||||
where(enabled_feature)
|
||||
}
|
||||
|
||||
# Picks a feature where the level is exactly that given.
|
||||
scope :with_feature_access_level, ->(feature, level) {
|
||||
feature_access_level_attribute = access_level_attribute(feature)
|
||||
where(project_features: { feature_access_level_attribute => level })
|
||||
}
|
||||
|
||||
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
|
||||
# they are only available to team members. This scope returns features where
|
||||
# the feature is either public, enabled, or internal with permission for the user.
|
||||
# Note: this scope doesn't enforce that the user has access to the projects, it just checks
|
||||
# that the user has access to the feature. It's important to use this scope with others
|
||||
# that checks project authorizations first (e.g. `filter_by_feature_visibility`).
|
||||
#
|
||||
# This method uses an optimised version of `with_feature_access_level` for
|
||||
# logged in users to more efficiently get private projects with the given
|
||||
# feature.
|
||||
def self.with_feature_available_for_user(feature, user)
|
||||
visible = [ENABLED, PUBLIC]
|
||||
|
||||
if user&.can_read_all_resources?
|
||||
with_feature_enabled(feature)
|
||||
elsif user
|
||||
min_access_level = required_minimum_access_level(feature)
|
||||
column = quoted_access_level_column(feature)
|
||||
|
||||
where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
|
||||
{
|
||||
public_visible: visible,
|
||||
private_visible: PRIVATE,
|
||||
authorizations: user.authorizations_for_projects(min_access_level: min_access_level, related_project_column: 'project_features.project_id')
|
||||
})
|
||||
else
|
||||
# This has to be added to include features whose value is nil in the db
|
||||
visible << nil
|
||||
with_feature_access_level(feature, visible)
|
||||
end
|
||||
end
|
||||
|
||||
def public_pages?
|
||||
return true unless Gitlab.config.pages.access_control
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.form-group
|
||||
%b= s_('ProjectSettings|Merge commit message template')
|
||||
%p.text-secondary
|
||||
- configure_the_merge_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md', anchor: 'merge-commit-message-template')
|
||||
- configure_the_merge_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md')
|
||||
- configure_the_merge_commit_message_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_merge_commit_message_help_link_url }
|
||||
= s_('ProjectSettings|The commit message used when merging, if the merge method creates a merge commit. %{link_start}Learn more about syntax and variables.%{link_end}').html_safe % { link_start: configure_the_merge_commit_message_help_link_start, link_end: '</a>'.html_safe }
|
||||
.mb-2
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.form-group
|
||||
%b= s_('ProjectSettings|Squash commit message template')
|
||||
%p.text-secondary
|
||||
- configure_the_squash_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md', anchor: 'squash-commit-message-template')
|
||||
- configure_the_squash_commit_message_help_link_url = help_page_path('user/project/merge_requests/commit_templates.md')
|
||||
- configure_the_squash_commit_message_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_squash_commit_message_help_link_url }
|
||||
= s_('ProjectSettings|The commit message used when squashing commits. %{link_start}Learn more about syntax and variables.%{link_end}').html_safe % { link_start: configure_the_squash_commit_message_help_link_start, link_end: '</a>'.html_safe }
|
||||
.mb-2
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
all_changed_files = helper.all_changed_files
|
||||
|
||||
def get_ci_config_files(files)
|
||||
files.select do |file|
|
||||
file.include?('gitlab/ci/config/entry')
|
||||
end
|
||||
end
|
||||
|
||||
schema_path = 'app/assets/javascripts/editor/schema/ci.json'
|
||||
has_schema_update = all_changed_files.include?(schema_path)
|
||||
return if has_schema_update
|
||||
|
||||
ci_config_files = get_ci_config_files(all_changed_files)
|
||||
return if ci_config_files.empty?
|
||||
|
||||
file_list = "- #{ci_config_files.map { |path| "`#{path}`" }.join("\n- ")}"
|
||||
|
||||
warn "This merge request changed CI config files but did not update the schema. Please consider updating [the schema](#{schema_path}) to reflect these changes:\n#{file_list}"
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ImproveIndexOnEventsForCalendar < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_events_author_id_project_id_action_target_type_created_at'
|
||||
|
||||
def up
|
||||
prepare_async_index :events, [:author_id, :project_id, :action, :target_type, :created_at], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index :events, [:author_id, :project_id, :action, :target_type, :created_at], name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
e010b4c12ae8203d9ea8a4c2035be5e7165aba0030f4d5fd0b0f978f84748707
|
|
@ -16,7 +16,7 @@ The configuration for doing so depends on your desired outcome.
|
|||
|
||||
## Make the repositories read-only
|
||||
|
||||
The first thing you'll want to accomplish is to ensure that no changes can be
|
||||
The first thing you want to accomplish is to ensure that no changes can be
|
||||
made to your repositories. There's two ways you can accomplish that:
|
||||
|
||||
- Either stop Puma to make the internal API unreachable:
|
||||
|
@ -46,7 +46,7 @@ made to your repositories. There's two ways you can accomplish that:
|
|||
## Shut down the GitLab UI
|
||||
|
||||
If you don't mind shutting down the GitLab UI, then the easiest approach is to
|
||||
stop `sidekiq` and `puma`, and you'll effectively ensure that no
|
||||
stop `sidekiq` and `puma`, and you effectively ensure that no
|
||||
changes can be made to GitLab:
|
||||
|
||||
```shell
|
||||
|
@ -63,7 +63,7 @@ sudo gitlab-ctl start puma
|
|||
|
||||
## Make the database read-only
|
||||
|
||||
If you want to allow users to use the GitLab UI, then you'll need to ensure that
|
||||
If you want to allow users to use the GitLab UI, then you need to ensure that
|
||||
the database is read-only:
|
||||
|
||||
1. Take a [GitLab backup](../raketasks/backup_restore.md)
|
||||
|
@ -113,7 +113,7 @@ the database is read-only:
|
|||
sudo gitlab-ctl restart postgresql
|
||||
```
|
||||
|
||||
When you're ready to revert the read-only state, you'll need to remove the added
|
||||
When you're ready to revert the read-only state, you need to remove the added
|
||||
lines in `/etc/gitlab/gitlab.rb`, and reconfigure GitLab and restart PostgreSQL:
|
||||
|
||||
```shell
|
||||
|
|
|
@ -850,6 +850,32 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
|||
"https://gitlab.example.com/api/v4/groups/4/projects/56"
|
||||
```
|
||||
|
||||
## Transfer a group to a new parent group / Turn a subgroup to a top-level group
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23831) in GitLab 14.6.
|
||||
|
||||
Transfer a group to a new parent group or turn a subgroup to a top-level group. Available to administrators and users:
|
||||
|
||||
- With the Owner role for the group to transfer.
|
||||
- With permission to [create a subgroup](../user/group/subgroups/index.md#creating-a-subgroup) in the new parent group if transferring a group.
|
||||
- With [permission to create a top-level group](../administration/user_settings.md#prevent-users-from-creating-top-level-groups) if turning a subgroup into a top-level group.
|
||||
|
||||
```plaintext
|
||||
POST /groups/:id/transfer
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------ | -------------- | -------- | ----------- |
|
||||
| `id` | integer | yes | ID of the group to transfer. |
|
||||
| `group_id` | integer | no | ID of the new parent group. When not specified, the group to transfer is instead turned into a top-level group. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/groups/4/transfer?group_id=7"
|
||||
```
|
||||
|
||||
## Update group
|
||||
|
||||
Updates the project group. Only available to group owners and administrators.
|
||||
|
|
|
@ -634,6 +634,65 @@ Example request:
|
|||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/approve_all"
|
||||
```
|
||||
|
||||
## List pending members of a group and its subgroups and projects
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332596) in GitLab 14.6.
|
||||
|
||||
For a group and its subgroups and projects, get a list of all members in an `awaiting` state and those who are invited but do not have a GitLab account.
|
||||
|
||||
This request returns all matching group and project members from all groups and projects in the root group's hierarchy.
|
||||
|
||||
When the member is an invited user that has not signed up for a GitLab account yet, the invited email address is returned.
|
||||
|
||||
This API endpoint works on top-level groups only. It does not work on subgroups.
|
||||
|
||||
This API endpoint requires permission to administer members for the group.
|
||||
|
||||
This API endpoint takes [pagination](index.md#pagination) parameters `page` and `per_page` to restrict the list of members.
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/pending_members
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/pending_members"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 168,
|
||||
"name": "Alex Garcia",
|
||||
"username": "alex_garcia",
|
||||
"email": "alex@example.com",
|
||||
"avatar_url": "http://example.com/uploads/user/avatar/1/cd8.jpeg",
|
||||
"web_url": "http://example.com/alex_garcia",
|
||||
"approved": false,
|
||||
"invited": false
|
||||
},
|
||||
{
|
||||
"id": 169,
|
||||
"email": "sidney@example.com",
|
||||
"avatar_url": "http://gravatar.com/../e346561cd8.jpeg",
|
||||
"approved": false,
|
||||
"invited": true
|
||||
},
|
||||
{
|
||||
"id": 170,
|
||||
"email": "zhang@example.com",
|
||||
"avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
|
||||
"approved": true,
|
||||
"invited": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Give a group access to a project
|
||||
|
||||
See [share project with group](projects.md#share-project-with-group)
|
||||
|
|
|
@ -1314,7 +1314,7 @@ POST /projects/user/:user_id
|
|||
| `jobs_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable jobs for this project. Use `builds_access_level` instead. |
|
||||
| `lfs_enabled` | boolean | **{dotted-circle}** No | Enable LFS. |
|
||||
| `merge_commit_template` | string | **{dotted-circle}** No | [Template](../user/project/merge_requests/commit_templates.md) used to create merge commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.)_ |
|
||||
| `squash_commit_template` | string | **{dotted-circle}** No | [Template](../user/project/merge_requests/commit_templates.md#squash-commit-message-template) used to create squash commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) in GitLab 14.6.)_ |
|
||||
| `squash_commit_template` | string | **{dotted-circle}** No | [Template](../user/project/merge_requests/commit_templates.md) used to create squash commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) in GitLab 14.6.)_ |
|
||||
| `merge_method` | string | **{dotted-circle}** No | Set the [merge method](#project-merge-method) used. |
|
||||
| `merge_requests_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
|
||||
| `merge_requests_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable merge requests for this project. Use `merge_requests_access_level` instead. |
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
|
@ -228,7 +228,13 @@ must create an association between that project and the project you want to appl
|
|||
project you would like to link from the dropdown menu.
|
||||
1. Select **Save**.
|
||||
|
||||
![Security Policy Project](img/security_policy_project_v14_3.png)
|
||||
![Security Policy Project](img/security_policy_project_v14_6.png)
|
||||
|
||||
### Unlink Security Policy projects
|
||||
|
||||
Project owners can unlink Security Policy projects from development projects. To do this, follow
|
||||
the steps described in [Security Policy project selection](#security-policy-project-selection),
|
||||
but select the trash can icon in the modal.
|
||||
|
||||
### Scan Execution Policy editor
|
||||
|
||||
|
|
|
@ -7,15 +7,42 @@ type: reference, howto
|
|||
|
||||
# Commit message templates **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.
|
||||
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) squash commit templates in GitLab 14.6.
|
||||
|
||||
## Merge commit message template
|
||||
GitLab uses commit templates to create default messages for specific types of
|
||||
commits. These templates encourage commit messages to follow a particular format,
|
||||
or contain specific information. Users can override these templates when merging
|
||||
a merge request.
|
||||
|
||||
As a project maintainer, you're able to configure merge commit message template. It will be used during merge to
|
||||
create commit message. Template uses similar syntax to
|
||||
Commit templates use syntax similar to the syntax for
|
||||
[review suggestions](reviews/suggestions.md#configure-the-commit-message-for-applied-suggestions).
|
||||
|
||||
Default merge commit message can be recreated using following template:
|
||||
## Configure commit templates
|
||||
|
||||
Change the commit templates for your project if the default templates don't
|
||||
contain the information you need.
|
||||
|
||||
Prerequisite:
|
||||
|
||||
- You must have at least the Maintainer role for a project.
|
||||
|
||||
To do this:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > General** and expand **Merge requests**.
|
||||
1. Depending on the type of template you want to create, scroll to either
|
||||
[**Merge commit message template**](#default-template-for-merge-commits) or
|
||||
[**Squash commit message template**](#default-template-for-squash-commits).
|
||||
1. For your desired commit type, enter your default message. You can use both static
|
||||
text and [variables](#supported-variables-in-commit-templates). Each template
|
||||
is limited to a maximum of 500 characters, though after replacing the templates
|
||||
with data, the final message may be longer.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Default template for merge commits
|
||||
|
||||
The default template for merge commit messages is:
|
||||
|
||||
```plaintext
|
||||
Merge branch '%{source_branch}' into '%{target_branch}'
|
||||
|
@ -27,40 +54,30 @@ Merge branch '%{source_branch}' into '%{target_branch}'
|
|||
See merge request %{reference}
|
||||
```
|
||||
|
||||
This commit message can be customized to follow any guidelines you might have.
|
||||
To do so, expand the **Merge requests** tab within your project's **General**
|
||||
settings and change the **Merge commit message template** text:
|
||||
## Default template for squash commits
|
||||
|
||||
![Custom commit message for merge commit](img/merge_commit_message_template_v14_5.png)
|
||||
|
||||
You can use static text and following variables:
|
||||
|
||||
| Variable | Description | Output example |
|
||||
|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
|
||||
| `%{source_branch}` | The name of the branch that is being merged. | `my-feature-branch` |
|
||||
| `%{target_branch}` | The name of the branch that the changes are applied to. | `master` |
|
||||
| `%{title}` | Title of the merge request. | Fix stuff |
|
||||
| `%{issues}` | String with phrase "Closes <issue numbers>" with all issues mentioned in the MR description matching [issue closing patterns](../issues/managing_issues.md#closing-issues-automatically). It will be empty when no issues were mentioned. | `Closes #465, #190 and #400` |
|
||||
| `%{description}` | Description of the merge request. | Merge request description.<br>Can be multiline. |
|
||||
| `%{reference}` | Reference to the merge request. | group-name/project-name!72359 |
|
||||
|
||||
NOTE:
|
||||
Empty variables that are the only word in a line will be removed along with all newline characters preceding it.
|
||||
|
||||
Merge commit template field has a limit of 500 characters. This limit only applies to the template
|
||||
itself.
|
||||
|
||||
## Squash commit message template
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) in GitLab 14.6.
|
||||
|
||||
As a project maintainer, you're able to configure squash commit message template. It will be used during merge with
|
||||
squash to create squash commit message. It uses the same syntax and variables as merge commit message template.
|
||||
|
||||
![Custom commit message for squash commit](img/squash_commit_message_template_v14_6.png)
|
||||
|
||||
Default squash commit message can be recreated using following template:
|
||||
If you have configured your project to [squash commits on merge](squash_and_merge.md),
|
||||
GitLab creates a squash commit message with this template:
|
||||
|
||||
```plaintext
|
||||
%{title}
|
||||
```
|
||||
|
||||
## Supported variables in commit templates
|
||||
|
||||
Commit message templates support these variables:
|
||||
|
||||
| Variable | Description | Output example |
|
||||
|----------|-------------|----------------|
|
||||
| `%{source_branch}` | The name of the branch being merged. | `my-feature-branch` |
|
||||
| `%{target_branch}` | The name of the branch that the changes are applied to. | `main` |
|
||||
| `%{title}` | Title of the merge request. | `Fix tests and translations` |
|
||||
| `%{issues}` | String with phrase `Closes <issue numbers>`. Contains all issues mentioned in the merge request description that match [issue closing patterns](../issues/managing_issues.md#closing-issues-automatically). Empty if no issues are mentioned. | `Closes #465, #190 and #400` |
|
||||
| `%{description}` | Description of the merge request. | `Merge request description.<br>Can be multiline.` |
|
||||
| `%{reference}` | Reference to the merge request. | `group-name/project-name!72359` |
|
||||
|
||||
Empty variables that are the only word in a line are removed, along with all newline characters preceding it.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Squash and merge](squash_and_merge.md).
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
|
@ -28,7 +28,8 @@ NOTE:
|
|||
The squashed commit in this example is followed by a merge commit, because the merge method for this repository uses a merge commit. You can disable merge commits in
|
||||
**Project Settings > General > Merge requests > Merge method > Fast-forward merge**.
|
||||
|
||||
The squashed commit's default commit message is taken from the merge request title. It can be changed using [squash commit message template](commit_templates.md#squash-commit-message-template).
|
||||
The squashed commit's default commit message is taken from the merge request title.
|
||||
You can [edit the default message for squash commits](commit_templates.md).
|
||||
|
||||
It can also be customized before merging a merge request.
|
||||
|
||||
|
@ -123,6 +124,10 @@ NOTE:
|
|||
If your project is set to **Do not allow** Squash and Merge, the users still have the option to
|
||||
squash commits locally through the command line and force-push to their remote branch before merging.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Commit message templates](commit_templates.md).
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -382,6 +382,28 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
desc 'Transfer a group to a new parent group or promote a subgroup to a root group'
|
||||
params do
|
||||
optional :group_id, type: Integer,
|
||||
desc: 'The ID of the target group to which the group needs to be transferred to.'\
|
||||
'If not provided, the source group will be promoted to a root group.'
|
||||
end
|
||||
post ':id/transfer' do
|
||||
group = find_group!(params[:id])
|
||||
authorize! :admin_group, group
|
||||
|
||||
new_parent_group = find_group!(params[:group_id]) if params[:group_id].present?
|
||||
|
||||
service = ::Groups::TransferService.new(group, current_user)
|
||||
|
||||
if service.execute(new_parent_group)
|
||||
group.preload_shared_group_links
|
||||
present group, with: Entities::GroupDetail, current_user: current_user
|
||||
else
|
||||
render_api_error!(service.error, 400)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Share a group with a group' do
|
||||
success Entities::GroupDetail
|
||||
end
|
||||
|
|
|
@ -21,9 +21,9 @@ module Banzai
|
|||
FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}.+\z/.freeze
|
||||
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}.+\z/.freeze
|
||||
|
||||
CSS_SECTION = "ol > li a[href^=\"\##{FOOTNOTE_LINK_ID_PREFIX}\"]"
|
||||
CSS_SECTION = "section[data-footnotes]"
|
||||
XPATH_SECTION = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION).freeze
|
||||
CSS_FOOTNOTE = 'sup > a[id]'
|
||||
CSS_FOOTNOTE = 'sup > a[data-footnote-ref]'
|
||||
XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze
|
||||
|
||||
# only needed when feature flag use_cmark_renderer is turned off
|
||||
|
@ -37,20 +37,28 @@ module Banzai
|
|||
XPATH_SECTION_OLD = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION_OLD).freeze
|
||||
|
||||
def call
|
||||
xpath_section = Feature.enabled?(:use_cmark_renderer) ? XPATH_SECTION : XPATH_SECTION_OLD
|
||||
return doc unless first_footnote = doc.at_xpath(xpath_section)
|
||||
|
||||
# Sanitization stripped off the section wrapper - add it back in
|
||||
if Feature.enabled?(:use_cmark_renderer)
|
||||
first_footnote.parent.parent.parent.wrap('<section class="footnotes" data-footnotes>')
|
||||
# Sanitization stripped off the section class - add it back in
|
||||
return doc unless section_node = doc.at_xpath(XPATH_SECTION)
|
||||
|
||||
section_node.append_class('footnotes')
|
||||
else
|
||||
return doc unless first_footnote = doc.at_xpath(XPATH_SECTION_OLD)
|
||||
return doc unless first_footnote.parent
|
||||
|
||||
first_footnote.parent.wrap('<section class="footnotes">')
|
||||
end
|
||||
|
||||
rand_suffix = "-#{random_number}"
|
||||
modified_footnotes = {}
|
||||
|
||||
doc.xpath(XPATH_FOOTNOTE).each do |link_node|
|
||||
xpath_footnote = if Feature.enabled?(:use_cmark_renderer)
|
||||
XPATH_FOOTNOTE
|
||||
else
|
||||
Gitlab::Utils::Nokogiri.css_to_xpath('sup > a[id]')
|
||||
end
|
||||
|
||||
doc.xpath(xpath_footnote).each do |link_node|
|
||||
if Feature.enabled?(:use_cmark_renderer)
|
||||
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
|
||||
ref_num.gsub!(/[[:punct:]]/, '\\\\\&')
|
||||
|
@ -58,7 +66,8 @@ module Banzai
|
|||
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD)
|
||||
end
|
||||
|
||||
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath("li[id=#{fn_id(ref_num)}]")
|
||||
css = Feature.enabled?(:use_cmark_renderer) ? "section[data-footnotes] li[id=#{fn_id(ref_num)}]" : "li[id=#{fn_id(ref_num)}]"
|
||||
node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath(css)
|
||||
footnote_node = doc.at_xpath(node_xpath)
|
||||
|
||||
if footnote_node || modified_footnotes[ref_num]
|
||||
|
@ -69,7 +78,6 @@ module Banzai
|
|||
|
||||
# Sanitization stripped off class - add it back in
|
||||
link_node.parent.append_class('footnote-ref')
|
||||
link_node['data-footnote-ref'] = nil if Feature.enabled?(:use_cmark_renderer)
|
||||
|
||||
unless modified_footnotes[ref_num]
|
||||
footnote_node[:id] += rand_suffix
|
||||
|
@ -78,7 +86,6 @@ module Banzai
|
|||
if backref_node
|
||||
backref_node[:href] += rand_suffix
|
||||
backref_node.append_class('footnote-backref')
|
||||
backref_node['data-footnote-backref'] = nil if Feature.enabled?(:use_cmark_renderer)
|
||||
end
|
||||
|
||||
modified_footnotes[ref_num] = true
|
||||
|
|
|
@ -28,6 +28,13 @@ module Banzai
|
|||
allowlist[:attributes]['li'] = %w[id]
|
||||
allowlist[:transformers].push(self.class.remove_non_footnote_ids)
|
||||
|
||||
if Feature.enabled?(:use_cmark_renderer)
|
||||
# Allow section elements with data-footnotes attribute
|
||||
allowlist[:elements].push('section')
|
||||
allowlist[:attributes]['section'] = %w(data-footnotes)
|
||||
allowlist[:attributes]['a'].push('data-footnote-ref', 'data-footnote-backref')
|
||||
end
|
||||
|
||||
allowlist
|
||||
end
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ module Gitlab
|
|||
return unless log?
|
||||
|
||||
attributes = {
|
||||
class: self.class.name.to_s,
|
||||
pipeline_creation_caller: caller,
|
||||
project_id: project.id,
|
||||
pipeline_id: pipeline.id,
|
||||
|
|
|
@ -23,25 +23,28 @@ module Gitlab
|
|||
def activity_dates
|
||||
return @activity_dates if @activity_dates.present?
|
||||
|
||||
date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'"
|
||||
|
||||
# Can't use Event.contributions here because we need to check 3 different
|
||||
# project_features for the (currently) 3 different contribution types
|
||||
date_from = @contributor_time_instance.now.years_ago(1)
|
||||
repo_events = event_counts(date_from, :repository)
|
||||
.having(action: :pushed)
|
||||
issue_events = event_counts(date_from, :issues)
|
||||
.having(action: [:created, :closed], target_type: "Issue")
|
||||
mr_events = event_counts(date_from, :merge_requests)
|
||||
.having(action: [:merged, :created, :closed], target_type: "MergeRequest")
|
||||
note_events = event_counts(date_from, :merge_requests)
|
||||
.having(action: :commented)
|
||||
repo_events = event_created_at(date_from, :repository)
|
||||
.where(action: :pushed, target_type: nil)
|
||||
issue_events = event_created_at(date_from, :issues)
|
||||
.where(action: [:created, :closed], target_type: "Issue")
|
||||
mr_events = event_created_at(date_from, :merge_requests)
|
||||
.where(action: [:merged, :created, :closed], target_type: "MergeRequest")
|
||||
note_events = event_created_at(date_from, :merge_requests)
|
||||
.where(action: :commented, target_type: "Note")
|
||||
|
||||
events = Event
|
||||
.select(:project_id, :target_type, :action, :date, :total_amount)
|
||||
.from_union([repo_events, issue_events, mr_events, note_events])
|
||||
.select("date(created_at + #{date_interval}) AS date", 'COUNT(*) AS num_events')
|
||||
.from_union([repo_events, issue_events, mr_events, note_events], remove_duplicates: false)
|
||||
.group(:date)
|
||||
.map(&:attributes)
|
||||
|
||||
@activity_dates = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
|
||||
activities[event["date"]] += event["total_amount"]
|
||||
activities[event["date"]] += event["num_events"]
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
@ -74,27 +77,25 @@ module Gitlab
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def event_counts(date_from, feature)
|
||||
def event_created_at(date_from, feature)
|
||||
t = Event.arel_table
|
||||
|
||||
# re-running the contributed projects query in each union is expensive, so
|
||||
# use IN(project_ids...) instead. It's the intersection of two users so
|
||||
# the list will be (relatively) short
|
||||
@contributed_project_ids ||= projects.distinct.pluck(:id)
|
||||
authed_projects = Project.where(id: @contributed_project_ids)
|
||||
authed_projects = ProjectFeature
|
||||
.with_feature_available_for_user(feature, current_user)
|
||||
.where(project_id: @contributed_project_ids)
|
||||
.reorder(nil)
|
||||
.select(:id)
|
||||
.select(:project_id)
|
||||
|
||||
conditions = t[:created_at].gteq(date_from.beginning_of_day)
|
||||
.and(t[:created_at].lteq(@contributor_time_instance.today.end_of_day))
|
||||
.and(t[:author_id].eq(contributor.id))
|
||||
|
||||
date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'"
|
||||
|
||||
Event.reorder(nil)
|
||||
.select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount')
|
||||
.group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})")
|
||||
.select(:created_at)
|
||||
.where(conditions)
|
||||
.where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
|
||||
end
|
||||
|
|
|
@ -180,5 +180,13 @@ module RuboCop
|
|||
def rails_root
|
||||
File.expand_path('..', __dir__)
|
||||
end
|
||||
|
||||
def ee?
|
||||
File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__)) && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
|
||||
end
|
||||
|
||||
def jh?
|
||||
ee? && Dir.exist?(File.expand_path('../jh', __dir__)) && !%w[true 1].include?(ENV['EE_ONLY'].to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -255,14 +255,12 @@ module RuboCop
|
|||
]
|
||||
|
||||
# For EE additionally process `ee/` feature flags
|
||||
is_ee = File.exist?(File.expand_path('../../../ee/app/models/license.rb', __dir__)) && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
|
||||
if is_ee
|
||||
if ee?
|
||||
flags_paths << 'ee/config/feature_flags/**/*.yml'
|
||||
end
|
||||
|
||||
# For JH additionally process `jh/` feature flags
|
||||
is_jh = is_ee && Dir.exist?(File.expand_path('../../../jh', __dir__)) && !%w[true 1].include?(ENV['EE_ONLY'].to_s)
|
||||
if is_jh
|
||||
if jh?
|
||||
flags_paths << 'jh/config/feature_flags/**/*.yml'
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module_function
|
||||
|
||||
def ee?
|
||||
File.exist?(File.expand_path('../../ee/app/models/license.rb', __dir__)) && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
|
||||
end
|
||||
|
||||
def jh?
|
||||
ee? && Dir.exist?(File.expand_path('../../jh', __dir__)) && !%w[true 1].include?(ENV['EE_ONLY'].to_s)
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
require 'set'
|
||||
require 'fileutils'
|
||||
require_relative 'lib/gitlab'
|
||||
|
||||
class String
|
||||
def red
|
||||
|
@ -27,8 +28,7 @@ flags_paths = [
|
|||
]
|
||||
|
||||
# For EE additionally process `ee/` feature flags
|
||||
is_ee = File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
|
||||
if is_ee
|
||||
if Gitlab.ee?
|
||||
flags_paths << 'ee/config/feature_flags/**/*.yml'
|
||||
|
||||
# Geo feature flags are constructed dynamically and there's no explicit checks in the codebase so we mark all
|
||||
|
@ -43,8 +43,7 @@ if is_ee
|
|||
end
|
||||
|
||||
# For JH additionally process `jh/` feature flags
|
||||
is_jh = is_ee && Dir.exist?('jh') && !%w[true 1].include?(ENV['EE_ONLY'].to_s)
|
||||
if is_jh
|
||||
if Gitlab.jh?
|
||||
flags_paths << 'jh/config/feature_flags/**/*.yml'
|
||||
|
||||
Dir.glob('jh/app/replicators/geo/*_replicator.rb').each_with_object(Set.new) do |path, memo|
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Banzai::Filter::FootnoteFilter do
|
||||
include FilterSpecHelper
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
# rubocop:disable Style/AsciiComments
|
||||
# first[^1] and second[^second] and third[^_😄_]
|
||||
|
@ -13,16 +14,16 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
|
|||
# rubocop:enable Style/AsciiComments
|
||||
let(:footnote) do
|
||||
<<~EOF.strip_heredoc
|
||||
<p>first<sup><a href="#fn-1" id="fnref-1">1</a></sup> and second<sup><a href="#fn-second" id="fnref-second">2</a></sup> and third<sup><a href="#fn-_%F0%9F%98%84_" id="fnref-_%F0%9F%98%84_">3</a></sup></p>
|
||||
|
||||
<p>first<sup><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> and second<sup><a href="#fn-second" id="fnref-second" data-footnote-ref>2</a></sup> and third<sup><a href="#fn-_%F0%9F%98%84_" id="fnref-_%F0%9F%98%84_" data-footnote-ref>3</a></sup></p>
|
||||
<section data-footnotes>
|
||||
<ol>
|
||||
<li id="fn-1">
|
||||
<p>one <a href="#fnref-1" aria-label="Back to content">↩</a></p>
|
||||
<p>one <a href="#fnref-1" aria-label="Back to content" data-footnote-backref>↩</a></p>
|
||||
</li>
|
||||
<li id="fn-second">
|
||||
<p>two <a href="#fnref-second" aria-label="Back to content">↩</a></p>
|
||||
<p>two <a href="#fnref-second" aria-label="Back to content" data-footnote-backref>↩</a></p>
|
||||
</li>\n<li id="fn-_%F0%9F%98%84_">
|
||||
<p>three <a href="#fnref-_%F0%9F%98%84_" aria-label="Back to content">↩</a></p>
|
||||
<p>three <a href="#fnref-_%F0%9F%98%84_" aria-label="Back to content" data-footnote-backref>↩</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
EOF
|
||||
|
@ -30,19 +31,20 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
|
|||
|
||||
let(:filtered_footnote) do
|
||||
<<~EOF.strip_heredoc
|
||||
<p>first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-second-#{identifier}" id="fnref-second-#{identifier}" data-footnote-ref="">2</a></sup> and third<sup class="footnote-ref"><a href="#fn-_%F0%9F%98%84_-#{identifier}" id="fnref-_%F0%9F%98%84_-#{identifier}" data-footnote-ref="">3</a></sup></p>
|
||||
|
||||
<section class=\"footnotes\" data-footnotes><ol>
|
||||
<p>first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref>1</a></sup> and second<sup class="footnote-ref"><a href="#fn-second-#{identifier}" id="fnref-second-#{identifier}" data-footnote-ref>2</a></sup> and third<sup class="footnote-ref"><a href="#fn-_%F0%9F%98%84_-#{identifier}" id="fnref-_%F0%9F%98%84_-#{identifier}" data-footnote-ref>3</a></sup></p>
|
||||
<section data-footnotes class=\"footnotes\">
|
||||
<ol>
|
||||
<li id="fn-1-#{identifier}">
|
||||
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
|
||||
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" data-footnote-backref class="footnote-backref">↩</a></p>
|
||||
</li>
|
||||
<li id="fn-second-#{identifier}">
|
||||
<p>two <a href="#fnref-second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
|
||||
<p>two <a href="#fnref-second-#{identifier}" aria-label="Back to content" data-footnote-backref class="footnote-backref">↩</a></p>
|
||||
</li>
|
||||
<li id="fn-_%F0%9F%98%84_-#{identifier}">
|
||||
<p>three <a href="#fnref-_%F0%9F%98%84_-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
|
||||
<p>three <a href="#fnref-_%F0%9F%98%84_-#{identifier}" aria-label="Back to content" data-footnote-backref class="footnote-backref">↩</a></p>
|
||||
</li>
|
||||
</ol></section>
|
||||
</ol>
|
||||
</section>
|
||||
EOF
|
||||
end
|
||||
|
||||
|
@ -52,7 +54,7 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
|
|||
let(:identifier) { link_node[:id].delete_prefix('fnref-1-') }
|
||||
|
||||
it 'properly adds the necessary ids and classes' do
|
||||
expect(doc.to_html).to eq filtered_footnote
|
||||
expect(doc.to_html).to eq filtered_footnote.strip
|
||||
end
|
||||
|
||||
context 'using ruby-based HTML renderer' do
|
||||
|
@ -101,4 +103,21 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when detecting footnotes' do
|
||||
where(:valid, :markdown) do
|
||||
true | "1. one[^1]\n[^1]: AbC"
|
||||
true | "1. one[^abc]\n[^abc]: AbC"
|
||||
false | '1. [one](#fnref-abc)'
|
||||
false | "1. one[^1]\n[^abc]: AbC"
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'detects valid footnotes' do
|
||||
result = Banzai::Pipeline::FullPipeline.call(markdown, project: nil)
|
||||
|
||||
expect(result[:output].at_css('section.footnotes').present?).to eq(valid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,26 +43,27 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
|
|||
|
||||
let(:filtered_footnote) do
|
||||
<<~EOF.strip_heredoc
|
||||
<p dir="auto">first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-%F0%9F%98%84second-#{identifier}" id="fnref-%F0%9F%98%84second-#{identifier}" data-footnote-ref="">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn-_twenty-#{identifier}" id="fnref-_twenty-#{identifier}" data-footnote-ref="">3</a></sup></p>
|
||||
|
||||
<section class="footnotes" data-footnotes><ol>
|
||||
<p dir="auto">first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref>1</a></sup> and second<sup class="footnote-ref"><a href="#fn-%F0%9F%98%84second-#{identifier}" id="fnref-%F0%9F%98%84second-#{identifier}" data-footnote-ref>2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn-_twenty-#{identifier}" id="fnref-_twenty-#{identifier}" data-footnote-ref>3</a></sup></p>
|
||||
<section data-footnotes class="footnotes">
|
||||
<ol>
|
||||
<li id="fn-1-#{identifier}">
|
||||
<p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
<p>one <a href="#fnref-1-#{identifier}" data-footnote-backref aria-label="Back to content" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
<li id="fn-%F0%9F%98%84second-#{identifier}">
|
||||
<p>two <a href="#fnref-%F0%9F%98%84second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
<p>two <a href="#fnref-%F0%9F%98%84second-#{identifier}" data-footnote-backref aria-label="Back to content" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
<li id="fn-_twenty-#{identifier}">
|
||||
<p>twenty <a href="#fnref-_twenty-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
<p>twenty <a href="#fnref-_twenty-#{identifier}" data-footnote-backref aria-label="Back to content" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
|
||||
</li>
|
||||
</ol></section>
|
||||
</ol>
|
||||
</section>
|
||||
EOF
|
||||
end
|
||||
|
||||
it 'properly adds the necessary ids and classes' do
|
||||
stub_commonmark_sourcepos_disabled
|
||||
|
||||
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
|
||||
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote.strip
|
||||
end
|
||||
|
||||
context 'using ruby-based HTML renderer' do
|
||||
|
|
|
@ -71,6 +71,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
|
|||
|
||||
let(:loggable_data) do
|
||||
{
|
||||
'class' => described_class.name.to_s,
|
||||
'pipeline_id' => pipeline.id,
|
||||
'pipeline_persisted' => true,
|
||||
'project_id' => project.id,
|
||||
|
|
|
@ -1943,6 +1943,116 @@ RSpec.describe API::Groups do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST /groups/:id/transfer' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:new_parent_group) { create(:group, :private) }
|
||||
let_it_be_with_reload(:group) { create(:group, :nested, :private) }
|
||||
|
||||
before do
|
||||
new_parent_group.add_owner(user)
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
def make_request(user)
|
||||
post api("/groups/#{group.id}/transfer", user), params: params
|
||||
end
|
||||
|
||||
context 'when promoting a subgroup to a root group' do
|
||||
shared_examples_for 'promotes the subgroup to a root group' do
|
||||
it 'returns success' do
|
||||
make_request(user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['parent_id']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no group_id is specified' do
|
||||
let(:params) {}
|
||||
|
||||
it_behaves_like 'promotes the subgroup to a root group'
|
||||
end
|
||||
|
||||
context 'when group_id is specified as blank' do
|
||||
let(:params) { { group_id: '' } }
|
||||
|
||||
it_behaves_like 'promotes the subgroup to a root group'
|
||||
end
|
||||
|
||||
context 'when the group is already a root group' do
|
||||
let(:group) { create(:group) }
|
||||
let(:params) { { group_id: '' } }
|
||||
|
||||
it 'returns error' do
|
||||
make_request(user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']).to eq('Transfer failed: Group is already a root group.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transferring a subgroup to a different group' do
|
||||
let(:params) { { group_id: new_parent_group.id } }
|
||||
|
||||
context 'when the user does not have admin rights to the group being transferred' do
|
||||
it 'forbids the operation' do
|
||||
developer_user = create(:user)
|
||||
group.add_developer(developer_user)
|
||||
|
||||
make_request(developer_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have access to the new parent group' do
|
||||
it 'fails with 404' do
|
||||
user_without_access_to_new_parent_group = create(:user)
|
||||
group.add_owner(user_without_access_to_new_parent_group)
|
||||
|
||||
make_request(user_without_access_to_new_parent_group)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the ID of a non-existent group is mentioned as the new parent group' do
|
||||
let(:params) { { group_id: non_existing_record_id } }
|
||||
|
||||
it 'fails with 404' do
|
||||
make_request(user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the transfer fails due to an error' do
|
||||
before do
|
||||
expect_next_instance_of(::Groups::TransferService) do |service|
|
||||
expect(service).to receive(:proceed_to_transfer).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
make_request(user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']).to eq('Transfer failed: namespace directory cannot be moved')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the transfer succceds' do
|
||||
it 'returns success' do
|
||||
make_request(user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['parent_id']).to eq(new_parent_group.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'custom attributes endpoints', 'groups' do
|
||||
let(:attributable) { group1 }
|
||||
let(:other_attributable) { group2 }
|
||||
|
|
|
@ -21,6 +21,8 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
end.new
|
||||
end
|
||||
|
||||
let(:ee_file_path) { File.expand_path('../../ee/app/models/license.rb', __dir__) }
|
||||
|
||||
describe '#send_to_constant?' do
|
||||
it 'returns true when sending to a constant' do
|
||||
node = build_and_parse_source('Foo.bar')
|
||||
|
@ -312,4 +314,77 @@ RSpec.describe RuboCop::CodeReuseHelpers do
|
|||
cop.disallow_send_to(def_node, 'Finder', 'oops')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ee?' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { true }
|
||||
end
|
||||
|
||||
it 'returns true when ee/app/models/license.rb exists' do
|
||||
expect(cop.ee?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#jh?' do
|
||||
context 'when jh directory exists and EE_ONLY is not set' do
|
||||
before do
|
||||
stub_env('EE_ONLY', nil)
|
||||
|
||||
allow(Dir).to receive(:exist?).with(File.expand_path('../../jh', __dir__)) { true }
|
||||
end
|
||||
|
||||
context 'when ee/app/models/license.rb exists' do
|
||||
before do
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { true }
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is not set' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(cop.jh?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is set to 1' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', '1')
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(cop.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ee/app/models/license.rb not exist' do
|
||||
before do
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { false }
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is not set' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(cop.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is set to 1' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', '1')
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(cop.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require_relative '../../../scripts/lib/gitlab'
|
||||
|
||||
RSpec.describe 'scripts/lib/gitlab.rb' do
|
||||
let(:ee_file_path) { File.expand_path('../../../ee/app/models/license.rb', __dir__) }
|
||||
|
||||
describe '.ee?' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { true }
|
||||
end
|
||||
|
||||
it 'returns true when ee/app/models/license.rb exists' do
|
||||
expect(Gitlab.ee?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.jh?' do
|
||||
context 'when jh directory exists and EE_ONLY is not set' do
|
||||
before do
|
||||
stub_env('EE_ONLY', nil)
|
||||
|
||||
allow(Dir).to receive(:exist?).with(File.expand_path('../../../jh', __dir__)) { true }
|
||||
end
|
||||
|
||||
context 'when ee/app/models/license.rb exists' do
|
||||
before do
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { true }
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is not set' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(Gitlab.jh?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is set to 1' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', '1')
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(Gitlab.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ee/app/models/license.rb not exist' do
|
||||
before do
|
||||
allow(File).to receive(:exist?).with(ee_file_path) { false }
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is not set' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', nil)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(Gitlab.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when FOSS_ONLY is set to 1' do
|
||||
before do
|
||||
stub_env('FOSS_ONLY', '1')
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(Gitlab.jh?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -284,7 +284,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
|
|||
|
||||
describe '.local_warning_message' do
|
||||
it 'returns an informational message with rules that can run' do
|
||||
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, documentation, duplicate_yarn_dependencies, eslint, gitaly, pajamas, pipeline, prettier, product_intelligence, utility_css, vue_shared_documentation')
|
||||
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, ci_config, database, documentation, duplicate_yarn_dependencies, eslint, gitaly, pajamas, pipeline, prettier, product_intelligence, utility_css, vue_shared_documentation')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ module Tooling
|
|||
module ProjectHelper
|
||||
LOCAL_RULES ||= %w[
|
||||
changelog
|
||||
ci_config
|
||||
database
|
||||
documentation
|
||||
duplicate_yarn_dependencies
|
||||
|
|
Loading…
Reference in New Issue