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-qa-base | ||||||
|     - .review:rules:review-qa-reliable |     - .review:rules:review-qa-reliable | ||||||
|     - .parallel-qa-base |     - .parallel-qa-base | ||||||
|  |   retry: 1 | ||||||
|   variables: |   variables: | ||||||
|     QA_RUN_TYPE: review-qa-reliable |     QA_RUN_TYPE: review-qa-reliable | ||||||
|     QA_SCENARIO: Test::Instance::Reliable |     QA_SCENARIO: Test::Instance::Reliable | ||||||
|  |  | ||||||
|  | @ -1 +1 @@ | ||||||
| a191a5d10f0772ae2ed6ec869001ddde6d277827 | 2247949bf06c7e8b28ef629dccb347868ee3abf4 | ||||||
|  |  | ||||||
|  | @ -49,6 +49,7 @@ fragment EpicNode on Epic { | ||||||
|     __typename |     __typename | ||||||
|     nodes { |     nodes { | ||||||
|       __typename |       __typename | ||||||
|  |       id | ||||||
|       color |       color | ||||||
|       description |       description | ||||||
|       textColor |       textColor | ||||||
|  | @ -140,6 +141,7 @@ query childItems( | ||||||
|               __typename |               __typename | ||||||
|               nodes { |               nodes { | ||||||
|                 __typename |                 __typename | ||||||
|  |                 id | ||||||
|                 color |                 color | ||||||
|                 description |                 description | ||||||
|                 textColor |                 textColor | ||||||
|  |  | ||||||
|  | @ -576,18 +576,12 @@ class Project < ApplicationRecord | ||||||
|       .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") |       .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # "enabled" here means "not disabled". It includes private features! |  | ||||||
|   scope :with_feature_enabled, ->(feature) { |   scope :with_feature_enabled, ->(feature) { | ||||||
|     access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)] |     with_project_feature.merge(ProjectFeature.with_feature_enabled(feature)) | ||||||
|     enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil)) |  | ||||||
| 
 |  | ||||||
|     with_project_feature.where(enabled_feature) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   # Picks a feature where the level is exactly that given. |  | ||||||
|   scope :with_feature_access_level, ->(feature, level) { |   scope :with_feature_access_level, ->(feature, level) { | ||||||
|     access_level_attribute = ProjectFeature.access_level_attribute(feature) |     with_project_feature.merge(ProjectFeature.with_feature_access_level(feature, level)) | ||||||
|     with_project_feature.where(project_features: { access_level_attribute => level }) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   # Picks projects which use the given programming language |   # Picks projects which use the given programming language | ||||||
|  | @ -688,37 +682,8 @@ class Project < ApplicationRecord | ||||||
|     end |     end | ||||||
|   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) |   def self.with_feature_available_for_user(feature, user) | ||||||
|     visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC] |     with_project_feature.merge(ProjectFeature.with_feature_available_for_user(feature, user)) | ||||||
| 
 |  | ||||||
|     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 |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def self.projects_user_can(projects, user, action) |   def self.projects_user_can(projects, user, action) | ||||||
|  |  | ||||||
|  | @ -83,6 +83,52 @@ class ProjectFeature < ApplicationRecord | ||||||
|     end |     end | ||||||
|   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? |   def public_pages? | ||||||
|     return true unless Gitlab.config.pages.access_control |     return true unless Gitlab.config.pages.access_control | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| .form-group | .form-group | ||||||
|   %b= s_('ProjectSettings|Merge commit message template') |   %b= s_('ProjectSettings|Merge commit message template') | ||||||
|   %p.text-secondary |   %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 } |     - 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 } |     = 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 |   .mb-2 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| .form-group | .form-group | ||||||
|   %b= s_('ProjectSettings|Squash commit message template') |   %b= s_('ProjectSettings|Squash commit message template') | ||||||
|   %p.text-secondary |   %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 } |     - 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 } |     = 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 |   .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 | ## 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: | made to your repositories. There's two ways you can accomplish that: | ||||||
| 
 | 
 | ||||||
| - Either stop Puma to make the internal API unreachable: | - 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 | ## Shut down the GitLab UI | ||||||
| 
 | 
 | ||||||
| If you don't mind shutting down the GitLab UI, then the easiest approach is to | 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: | changes can be made to GitLab: | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
|  | @ -63,7 +63,7 @@ sudo gitlab-ctl start puma | ||||||
| 
 | 
 | ||||||
| ## Make the database read-only | ## 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: | the database is read-only: | ||||||
| 
 | 
 | ||||||
| 1. Take a [GitLab backup](../raketasks/backup_restore.md) | 1. Take a [GitLab backup](../raketasks/backup_restore.md) | ||||||
|  | @ -113,7 +113,7 @@ the database is read-only: | ||||||
|    sudo gitlab-ctl restart postgresql |    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: | lines in `/etc/gitlab/gitlab.rb`, and reconfigure GitLab and restart PostgreSQL: | ||||||
| 
 | 
 | ||||||
| ```shell | ```shell | ||||||
|  |  | ||||||
|  | @ -850,6 +850,32 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ | ||||||
|      "https://gitlab.example.com/api/v4/groups/4/projects/56" |      "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 | ## Update group | ||||||
| 
 | 
 | ||||||
| Updates the project group. Only available to group owners and administrators. | 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" | 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 | ## Give a group access to a project | ||||||
| 
 | 
 | ||||||
| See [share project with group](projects.md#share-project-with-group) | 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. | | | `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. | | | `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.)_ | | | `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_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_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. | | | `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. |    project you would like to link from the dropdown menu. | ||||||
| 1. Select **Save**. | 1. Select **Save**. | ||||||
| 
 | 
 | ||||||
|     |     | ||||||
|  | 
 | ||||||
|  | ### 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 | ### Scan Execution Policy editor | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,15 +7,42 @@ type: reference, howto | ||||||
| 
 | 
 | ||||||
| # Commit message templates **(FREE)** | # 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 | Commit templates use syntax similar to the syntax for | ||||||
| create commit message. Template uses similar syntax to  |  | ||||||
| [review suggestions](reviews/suggestions.md#configure-the-commit-message-for-applied-suggestions). | [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 | ```plaintext | ||||||
| Merge branch '%{source_branch}' into '%{target_branch}' | Merge branch '%{source_branch}' into '%{target_branch}' | ||||||
|  | @ -27,40 +54,30 @@ Merge branch '%{source_branch}' into '%{target_branch}' | ||||||
| See merge request %{reference} | See merge request %{reference} | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| This commit message can be customized to follow any guidelines you might have. | ## Default template for squash commits | ||||||
| To do so, expand the **Merge requests** tab within your project's **General** |  | ||||||
| settings and change the **Merge commit message template** text: |  | ||||||
| 
 | 
 | ||||||
|  | If you have configured your project to [squash commits on merge](squash_and_merge.md), | ||||||
| 
 | GitLab creates a squash commit message with this template: | ||||||
| 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. |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
| 
 |  | ||||||
| Default squash commit message can be recreated using following template: |  | ||||||
| 
 | 
 | ||||||
| ```plaintext | ```plaintext | ||||||
| %{title} | %{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 | 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**. | **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. | 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 | 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. | 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 | <!-- ## Troubleshooting | ||||||
| 
 | 
 | ||||||
| Include any troubleshooting steps that you can foresee. If you know beforehand what issues | Include any troubleshooting steps that you can foresee. If you know beforehand what issues | ||||||
|  |  | ||||||
|  | @ -382,6 +382,28 @@ module API | ||||||
|         end |         end | ||||||
|       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 |       desc 'Share a group with a group' do | ||||||
|         success Entities::GroupDetail |         success Entities::GroupDetail | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -21,9 +21,9 @@ module Banzai | ||||||
|       FOOTNOTE_LI_REFERENCE_PATTERN   = /\A#{FOOTNOTE_ID_PREFIX}.+\z/.freeze |       FOOTNOTE_LI_REFERENCE_PATTERN   = /\A#{FOOTNOTE_ID_PREFIX}.+\z/.freeze | ||||||
|       FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_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 |       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 |       XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze | ||||||
| 
 | 
 | ||||||
|       # only needed when feature flag use_cmark_renderer is turned off |       # 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 |       XPATH_SECTION_OLD                   = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION_OLD).freeze | ||||||
| 
 | 
 | ||||||
|       def call |       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) |         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 |         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">') |           first_footnote.parent.wrap('<section class="footnotes">') | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         rand_suffix = "-#{random_number}" |         rand_suffix = "-#{random_number}" | ||||||
|         modified_footnotes = {} |         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) |           if Feature.enabled?(:use_cmark_renderer) | ||||||
|             ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) |             ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) | ||||||
|             ref_num.gsub!(/[[:punct:]]/, '\\\\\&') |             ref_num.gsub!(/[[:punct:]]/, '\\\\\&') | ||||||
|  | @ -58,7 +66,8 @@ module Banzai | ||||||
|             ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD) |             ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX_OLD) | ||||||
|           end |           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) |           footnote_node = doc.at_xpath(node_xpath) | ||||||
| 
 | 
 | ||||||
|           if footnote_node || modified_footnotes[ref_num] |           if footnote_node || modified_footnotes[ref_num] | ||||||
|  | @ -69,7 +78,6 @@ module Banzai | ||||||
| 
 | 
 | ||||||
|             # Sanitization stripped off class - add it back in |             # Sanitization stripped off class - add it back in | ||||||
|             link_node.parent.append_class('footnote-ref') |             link_node.parent.append_class('footnote-ref') | ||||||
|             link_node['data-footnote-ref'] = nil if Feature.enabled?(:use_cmark_renderer) |  | ||||||
| 
 | 
 | ||||||
|             unless modified_footnotes[ref_num] |             unless modified_footnotes[ref_num] | ||||||
|               footnote_node[:id] += rand_suffix |               footnote_node[:id] += rand_suffix | ||||||
|  | @ -78,7 +86,6 @@ module Banzai | ||||||
|               if backref_node |               if backref_node | ||||||
|                 backref_node[:href] += rand_suffix |                 backref_node[:href] += rand_suffix | ||||||
|                 backref_node.append_class('footnote-backref') |                 backref_node.append_class('footnote-backref') | ||||||
|                 backref_node['data-footnote-backref'] = nil if Feature.enabled?(:use_cmark_renderer) |  | ||||||
|               end |               end | ||||||
| 
 | 
 | ||||||
|               modified_footnotes[ref_num] = true |               modified_footnotes[ref_num] = true | ||||||
|  |  | ||||||
|  | @ -28,6 +28,13 @@ module Banzai | ||||||
|         allowlist[:attributes]['li'] = %w[id] |         allowlist[:attributes]['li'] = %w[id] | ||||||
|         allowlist[:transformers].push(self.class.remove_non_footnote_ids) |         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 |         allowlist | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,6 +47,7 @@ module Gitlab | ||||||
|           return unless log? |           return unless log? | ||||||
| 
 | 
 | ||||||
|           attributes = { |           attributes = { | ||||||
|  |             class: self.class.name.to_s, | ||||||
|             pipeline_creation_caller: caller, |             pipeline_creation_caller: caller, | ||||||
|             project_id: project.id, |             project_id: project.id, | ||||||
|             pipeline_id: pipeline.id, |             pipeline_id: pipeline.id, | ||||||
|  |  | ||||||
|  | @ -23,25 +23,28 @@ module Gitlab | ||||||
|     def activity_dates |     def activity_dates | ||||||
|       return @activity_dates if @activity_dates.present? |       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 |       # Can't use Event.contributions here because we need to check 3 different | ||||||
|       # project_features for the (currently) 3 different contribution types |       # project_features for the (currently) 3 different contribution types | ||||||
|       date_from = @contributor_time_instance.now.years_ago(1) |       date_from = @contributor_time_instance.now.years_ago(1) | ||||||
|       repo_events = event_counts(date_from, :repository) |       repo_events = event_created_at(date_from, :repository) | ||||||
|         .having(action: :pushed) |         .where(action: :pushed, target_type: nil) | ||||||
|       issue_events = event_counts(date_from, :issues) |       issue_events = event_created_at(date_from, :issues) | ||||||
|         .having(action: [:created, :closed], target_type: "Issue") |         .where(action: [:created, :closed], target_type: "Issue") | ||||||
|       mr_events = event_counts(date_from, :merge_requests) |       mr_events = event_created_at(date_from, :merge_requests) | ||||||
|         .having(action: [:merged, :created, :closed], target_type: "MergeRequest") |         .where(action: [:merged, :created, :closed], target_type: "MergeRequest") | ||||||
|       note_events = event_counts(date_from, :merge_requests) |       note_events = event_created_at(date_from, :merge_requests) | ||||||
|         .having(action: :commented) |         .where(action: :commented, target_type: "Note") | ||||||
| 
 | 
 | ||||||
|       events = Event |       events = Event | ||||||
|         .select(:project_id, :target_type, :action, :date, :total_amount) |         .select("date(created_at + #{date_interval}) AS date", 'COUNT(*) AS num_events') | ||||||
|         .from_union([repo_events, issue_events, mr_events, note_events]) |         .from_union([repo_events, issue_events, mr_events, note_events], remove_duplicates: false) | ||||||
|  |         .group(:date) | ||||||
|         .map(&:attributes) |         .map(&:attributes) | ||||||
| 
 | 
 | ||||||
|       @activity_dates = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities| |       @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 | ||||||
|     end |     end | ||||||
|     # rubocop: enable CodeReuse/ActiveRecord |     # rubocop: enable CodeReuse/ActiveRecord | ||||||
|  | @ -74,27 +77,25 @@ module Gitlab | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     # rubocop: disable CodeReuse/ActiveRecord |     # rubocop: disable CodeReuse/ActiveRecord | ||||||
|     def event_counts(date_from, feature) |     def event_created_at(date_from, feature) | ||||||
|       t = Event.arel_table |       t = Event.arel_table | ||||||
| 
 | 
 | ||||||
|       # re-running the contributed projects query in each union is expensive, so |       # 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 |       # use IN(project_ids...) instead. It's the intersection of two users so | ||||||
|       # the list will be (relatively) short |       # the list will be (relatively) short | ||||||
|       @contributed_project_ids ||= projects.distinct.pluck(:id) |       @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) |         .with_feature_available_for_user(feature, current_user) | ||||||
|  |         .where(project_id: @contributed_project_ids) | ||||||
|         .reorder(nil) |         .reorder(nil) | ||||||
|         .select(:id) |         .select(:project_id) | ||||||
| 
 | 
 | ||||||
|       conditions = t[:created_at].gteq(date_from.beginning_of_day) |       conditions = t[:created_at].gteq(date_from.beginning_of_day) | ||||||
|         .and(t[:created_at].lteq(@contributor_time_instance.today.end_of_day)) |         .and(t[:created_at].lteq(@contributor_time_instance.today.end_of_day)) | ||||||
|         .and(t[:author_id].eq(contributor.id)) |         .and(t[:author_id].eq(contributor.id)) | ||||||
| 
 | 
 | ||||||
|       date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'" |  | ||||||
| 
 |  | ||||||
|       Event.reorder(nil) |       Event.reorder(nil) | ||||||
|         .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') |         .select(:created_at) | ||||||
|         .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})") |  | ||||||
|         .where(conditions) |         .where(conditions) | ||||||
|         .where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection |         .where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -180,5 +180,13 @@ module RuboCop | ||||||
|     def rails_root |     def rails_root | ||||||
|       File.expand_path('..', __dir__) |       File.expand_path('..', __dir__) | ||||||
|     end |     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 | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -255,14 +255,12 @@ module RuboCop | ||||||
|             ] |             ] | ||||||
| 
 | 
 | ||||||
|             # For EE additionally process `ee/` feature flags |             # 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 ee? | ||||||
|             if is_ee |  | ||||||
|               flags_paths << 'ee/config/feature_flags/**/*.yml' |               flags_paths << 'ee/config/feature_flags/**/*.yml' | ||||||
|             end |             end | ||||||
| 
 | 
 | ||||||
|             # For JH additionally process `jh/` feature flags |             # 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 jh? | ||||||
|             if is_jh |  | ||||||
|               flags_paths << 'jh/config/feature_flags/**/*.yml' |               flags_paths << 'jh/config/feature_flags/**/*.yml' | ||||||
|             end |             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 'set' | ||||||
| require 'fileutils' | require 'fileutils' | ||||||
|  | require_relative 'lib/gitlab' | ||||||
| 
 | 
 | ||||||
| class String | class String | ||||||
|   def red |   def red | ||||||
|  | @ -27,8 +28,7 @@ flags_paths = [ | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| # For EE additionally process `ee/` feature flags | # 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 Gitlab.ee? | ||||||
| if is_ee |  | ||||||
|   flags_paths << 'ee/config/feature_flags/**/*.yml' |   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 |   # 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 | end | ||||||
| 
 | 
 | ||||||
| # For JH additionally process `jh/` feature flags | # For JH additionally process `jh/` feature flags | ||||||
| is_jh = is_ee && Dir.exist?('jh') && !%w[true 1].include?(ENV['EE_ONLY'].to_s) | if Gitlab.jh? | ||||||
| if is_jh |  | ||||||
|   flags_paths << 'jh/config/feature_flags/**/*.yml' |   flags_paths << 'jh/config/feature_flags/**/*.yml' | ||||||
| 
 | 
 | ||||||
|   Dir.glob('jh/app/replicators/geo/*_replicator.rb').each_with_object(Set.new) do |path, memo| |   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 | RSpec.describe Banzai::Filter::FootnoteFilter do | ||||||
|   include FilterSpecHelper |   include FilterSpecHelper | ||||||
|  |   using RSpec::Parameterized::TableSyntax | ||||||
| 
 | 
 | ||||||
|   # rubocop:disable Style/AsciiComments |   # rubocop:disable Style/AsciiComments | ||||||
|   # first[^1] and second[^second] and third[^_😄_] |   # first[^1] and second[^second] and third[^_😄_] | ||||||
|  | @ -13,16 +14,16 @@ RSpec.describe Banzai::Filter::FootnoteFilter do | ||||||
|   # rubocop:enable Style/AsciiComments |   # rubocop:enable Style/AsciiComments | ||||||
|   let(:footnote) do |   let(:footnote) do | ||||||
|     <<~EOF.strip_heredoc |     <<~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> |       <ol> | ||||||
|       <li id="fn-1"> |       <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> | ||||||
|       <li id="fn-second"> |       <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_"> |       </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> |       </li> | ||||||
|       </ol> |       </ol> | ||||||
|     EOF |     EOF | ||||||
|  | @ -30,19 +31,20 @@ RSpec.describe Banzai::Filter::FootnoteFilter do | ||||||
| 
 | 
 | ||||||
|   let(:filtered_footnote) do |   let(:filtered_footnote) do | ||||||
|     <<~EOF.strip_heredoc |     <<~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> |       <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\"> | ||||||
|       <section class=\"footnotes\" data-footnotes><ol> |       <ol> | ||||||
|       <li id="fn-1-#{identifier}"> |       <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> | ||||||
|       <li id="fn-second-#{identifier}"> |       <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> | ||||||
|       <li id="fn-_%F0%9F%98%84_-#{identifier}"> |       <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> |       </li> | ||||||
|       </ol></section> |       </ol> | ||||||
|  |       </section> | ||||||
|     EOF |     EOF | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -52,7 +54,7 @@ RSpec.describe Banzai::Filter::FootnoteFilter do | ||||||
|     let(:identifier) { link_node[:id].delete_prefix('fnref-1-') } |     let(:identifier) { link_node[:id].delete_prefix('fnref-1-') } | ||||||
| 
 | 
 | ||||||
|     it 'properly adds the necessary ids and classes' do |     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 |     end | ||||||
| 
 | 
 | ||||||
|     context 'using ruby-based HTML renderer' do |     context 'using ruby-based HTML renderer' do | ||||||
|  | @ -101,4 +103,21 @@ RSpec.describe Banzai::Filter::FootnoteFilter do | ||||||
|       end |       end | ||||||
|     end |     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 | end | ||||||
|  |  | ||||||
|  | @ -43,26 +43,27 @@ RSpec.describe Banzai::Pipeline::FullPipeline do | ||||||
| 
 | 
 | ||||||
|     let(:filtered_footnote) do |     let(:filtered_footnote) do | ||||||
|       <<~EOF.strip_heredoc |       <<~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> |         <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"> | ||||||
|         <section class="footnotes" data-footnotes><ol> |         <ol> | ||||||
|         <li id="fn-1-#{identifier}"> |         <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> | ||||||
|         <li id="fn-%F0%9F%98%84second-#{identifier}"> |         <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> | ||||||
|         <li id="fn-_twenty-#{identifier}"> |         <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> |         </li> | ||||||
|         </ol></section> |         </ol> | ||||||
|  |         </section> | ||||||
|       EOF |       EOF | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'properly adds the necessary ids and classes' do |     it 'properly adds the necessary ids and classes' do | ||||||
|       stub_commonmark_sourcepos_disabled |       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 |     end | ||||||
| 
 | 
 | ||||||
|     context 'using ruby-based HTML renderer' do |     context 'using ruby-based HTML renderer' do | ||||||
|  |  | ||||||
|  | @ -71,6 +71,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do | ||||||
| 
 | 
 | ||||||
|       let(:loggable_data) do |       let(:loggable_data) do | ||||||
|         { |         { | ||||||
|  |           'class' => described_class.name.to_s, | ||||||
|           'pipeline_id' => pipeline.id, |           'pipeline_id' => pipeline.id, | ||||||
|           'pipeline_persisted' => true, |           'pipeline_persisted' => true, | ||||||
|           'project_id' => project.id, |           'project_id' => project.id, | ||||||
|  |  | ||||||
|  | @ -1943,6 +1943,116 @@ RSpec.describe API::Groups do | ||||||
|     end |     end | ||||||
|   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 |   it_behaves_like 'custom attributes endpoints', 'groups' do | ||||||
|     let(:attributable) { group1 } |     let(:attributable) { group1 } | ||||||
|     let(:other_attributable) { group2 } |     let(:other_attributable) { group2 } | ||||||
|  |  | ||||||
|  | @ -21,6 +21,8 @@ RSpec.describe RuboCop::CodeReuseHelpers do | ||||||
|     end.new |     end.new | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   let(:ee_file_path) { File.expand_path('../../ee/app/models/license.rb', __dir__) } | ||||||
|  | 
 | ||||||
|   describe '#send_to_constant?' do |   describe '#send_to_constant?' do | ||||||
|     it 'returns true when sending to a constant' do |     it 'returns true when sending to a constant' do | ||||||
|       node = build_and_parse_source('Foo.bar') |       node = build_and_parse_source('Foo.bar') | ||||||
|  | @ -312,4 +314,77 @@ RSpec.describe RuboCop::CodeReuseHelpers do | ||||||
|       cop.disallow_send_to(def_node, 'Finder', 'oops') |       cop.disallow_send_to(def_node, 'Finder', 'oops') | ||||||
|     end |     end | ||||||
|   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 | 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 |   describe '.local_warning_message' do | ||||||
|     it 'returns an informational message with rules that can run' 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 | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ module Tooling | ||||||
|     module ProjectHelper |     module ProjectHelper | ||||||
|       LOCAL_RULES ||= %w[ |       LOCAL_RULES ||= %w[ | ||||||
|         changelog |         changelog | ||||||
|  |         ci_config | ||||||
|         database |         database | ||||||
|         documentation |         documentation | ||||||
|         duplicate_yarn_dependencies |         duplicate_yarn_dependencies | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue