Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2023b1313d
commit
ed01bf9b25
30 changed files with 750 additions and 340 deletions
|
@ -23,10 +23,10 @@ module Todos
|
|||
return if user_has_reporter_access?
|
||||
|
||||
remove_confidential_resource_todos
|
||||
remove_group_todos
|
||||
|
||||
if entity.private?
|
||||
remove_project_todos
|
||||
remove_group_todos
|
||||
else
|
||||
enqueue_private_features_worker
|
||||
end
|
||||
|
@ -68,7 +68,7 @@ module Todos
|
|||
return unless entity.is_a?(Namespace)
|
||||
|
||||
Todo
|
||||
.for_group(non_authorized_non_public_groups)
|
||||
.for_group(unauthorized_private_groups)
|
||||
.for_user(user)
|
||||
.delete_all
|
||||
end
|
||||
|
@ -104,16 +104,13 @@ module Todos
|
|||
GroupsFinder.new(user, min_access_level: Gitlab::Access::REPORTER).execute.select(:id)
|
||||
end
|
||||
|
||||
# since the entity is a private group, we can assume all subgroups are also
|
||||
# private. We can therefore limit GroupsFinder with `all_available: false`.
|
||||
# Otherwise it tries to include all public groups. This generates an expensive
|
||||
# SQL queries: https://gitlab.com/gitlab-org/gitlab/-/issues/325133
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def non_authorized_non_public_groups
|
||||
def unauthorized_private_groups
|
||||
return [] unless entity.is_a?(Namespace)
|
||||
return [] unless entity.private?
|
||||
|
||||
entity.self_and_descendants.select(:id)
|
||||
groups = entity.self_and_descendants.private_only
|
||||
|
||||
groups.select(:id)
|
||||
.id_not_in(GroupsFinder.new(user, all_available: false).execute.select(:id).reorder(nil))
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: active_record_transactions_tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67918
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338306
|
||||
milestone: '14.2'
|
||||
type: ops
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
18
config/initializers/active_record_transaction_observer.rb
Normal file
18
config/initializers/active_record_transaction_observer.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
return unless Gitlab.com? || Gitlab.dev_or_test_env?
|
||||
|
||||
def feature_flags_available?
|
||||
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
|
||||
active_db_connection = ActiveRecord::Base.connection.active? rescue false
|
||||
|
||||
active_db_connection && Feature::FlipperFeature.table_exists?
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
false
|
||||
end
|
||||
|
||||
Gitlab::Application.configure do
|
||||
if feature_flags_available? && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops, default_enabled: :yaml)
|
||||
Gitlab::Database::Transaction::Observer.register!
|
||||
end
|
||||
end
|
11
config/initializers/active_record_transaction_patches.rb
Normal file
11
config/initializers/active_record_transaction_patches.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if ENV['ACTIVE_RECORD_DISABLE_TRANSACTION_METRICS_PATCHES'].blank?
|
||||
Gitlab::Database.install_transaction_metrics_patches!
|
||||
end
|
||||
|
||||
return unless Gitlab.com? || Gitlab.dev_or_test_env?
|
||||
|
||||
if ENV['ACTIVE_RECORD_DISABLE_TRANSACTION_CONTEXT_PATCHES'].blank?
|
||||
Gitlab::Database.install_transaction_context_patches!
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Gitlab::Database.install_monkey_patches
|
|
@ -223,6 +223,28 @@ on the Gitaly server matches the one on Gitaly client. If it doesn't match,
|
|||
update the secrets file on the Gitaly server to match the Gitaly client, then
|
||||
[reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure).
|
||||
|
||||
### Repository pushes fail with a `deny updating a hidden ref` error
|
||||
|
||||
Due to [a change](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3426)
|
||||
introduced in GitLab 13.12, Gitaly has read-only, internal GitLab references that users are not
|
||||
permitted to update. If you attempt to update internal references with `git push --mirror`, Git
|
||||
returns the rejection error, `deny updating a hidden ref`.
|
||||
|
||||
The following references are read-only:
|
||||
|
||||
- refs/environments/
|
||||
- refs/keep-around/
|
||||
- refs/merge-requests/
|
||||
- refs/pipelines/
|
||||
|
||||
To mirror-push branches and tags only, and avoid attempting to mirror-push protected refs, run:
|
||||
|
||||
```shell
|
||||
git push origin +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/*
|
||||
```
|
||||
|
||||
Any other namespaces that the admin wants to push can be included there as well via additional patterns.
|
||||
|
||||
### Command line tools cannot connect to Gitaly
|
||||
|
||||
gRPC cannot reach your Gitaly server if:
|
||||
|
|
|
@ -6,12 +6,12 @@ type: reference
|
|||
last_update: 2019-07-03
|
||||
---
|
||||
|
||||
# Merge Trains **(PREMIUM)**
|
||||
# Merge trains **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9186) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.0.
|
||||
> - [Squash and merge](../../user/project/merge_requests/squash_and_merge.md) support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13001) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.6.
|
||||
|
||||
For more information about why you might want to use Merge Trains, read [How merge trains keep your master green](https://about.gitlab.com/blog/2020/01/30/all-aboard-merge-trains/).
|
||||
For more information about why you might want to use merge trains, read [How merge trains keep your master green](https://about.gitlab.com/blog/2020/01/30/all-aboard-merge-trains/).
|
||||
|
||||
When [pipelines for merged results](pipelines_for_merged_results.md) are
|
||||
enabled, the pipeline jobs run as if the changes from your source branch have already
|
||||
|
@ -63,7 +63,7 @@ Read more about [how merge trains keep your master green](https://about.gitlab.c
|
|||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
Watch this video for a demonstration on [how parallel execution
|
||||
of Merge Trains can prevent commits from breaking the default
|
||||
of merge trains can prevent commits from breaking the default
|
||||
branch](https://www.youtube.com/watch?v=D4qCqXgZkHQ).
|
||||
|
||||
## Prerequisites
|
||||
|
@ -83,11 +83,13 @@ To enable merge trains for your project:
|
|||
1. If you are on a self-managed GitLab instance, ensure the [feature flag](#merge-trains-feature-flag) is set correctly.
|
||||
1. [Configure your CI/CD configuration file](merge_request_pipelines.md#configure-pipelines-for-merge-requests)
|
||||
so that the pipeline or individual jobs run for merge requests.
|
||||
1. Visit your project's **Settings > General** and expand **Merge requests**.
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **Merge requests**.
|
||||
1. In the **Merge method** section, verify that **Merge commit** is selected.
|
||||
You cannot use **Merge commit with semi-linear history** or **Fast-forward merge** with merge trains.
|
||||
1. In the **Merge options** section, select **Enable merged results pipelines.** (if not already selected) and **Enable merge trains.**
|
||||
1. Click **Save changes**
|
||||
1. In the **Merge options** section, select **Enable merged results pipelines** (if not already selected) and **Enable merge trains**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
In GitLab 13.5 and earlier, there is only one checkbox, named
|
||||
**Enable merge trains and pipelines for merged results**.
|
||||
|
@ -102,7 +104,7 @@ unresolved state or your pipelines may be dropped.
|
|||
To start a merge train:
|
||||
|
||||
1. Visit a merge request.
|
||||
1. Click the **Start merge train** button.
|
||||
1. Select **Start merge train**.
|
||||
|
||||
![Start merge train](img/merge_train_start_v12_0.png)
|
||||
|
||||
|
@ -113,7 +115,7 @@ Other merge requests can now be added to the train.
|
|||
To add a merge request to a merge train:
|
||||
|
||||
1. Visit a merge request.
|
||||
1. Click the **Add to merge train** button.
|
||||
1. Select **Add to merge train**.
|
||||
|
||||
If pipelines are already running for the merge request, you cannot add the merge request
|
||||
to the train. Instead, you can schedule to add the merge request to a merge train **when the latest
|
||||
|
@ -124,7 +126,7 @@ pipeline succeeds**.
|
|||
## Remove a merge request from a merge train
|
||||
|
||||
1. Visit a merge request.
|
||||
1. Click the **Remove from merge train** button.
|
||||
1. Select **Remove from merge train**.
|
||||
|
||||
![Cancel merge train](img/merge_train_cancel_v12_0.png)
|
||||
|
||||
|
@ -161,23 +163,24 @@ In these cases, the reason for dropping the merge request is in the **system not
|
|||
To check the reason:
|
||||
|
||||
1. Open the merge request that was dropped from the merge train.
|
||||
1. Open the **Discussion** tab.
|
||||
1. Select the **Discussion** tab.
|
||||
1. Find a system note that includes either:
|
||||
- The text **... removed this merge request from the merge train because ...**
|
||||
- **... removed this merge request from the merge train because ...**
|
||||
- **... aborted this merge request from the merge train because ...**
|
||||
The reason is given in the text after the **because ...** phrase.
|
||||
|
||||
![Merge Train Failure](img/merge_train_failure.png)
|
||||
The reason is given in the text after the **because ...** phrase.
|
||||
|
||||
![Merge train failure](img/merge_train_failure.png)
|
||||
|
||||
### Merge When Pipeline Succeeds cannot be chosen
|
||||
|
||||
[Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md)
|
||||
is currently unavailable when Merge Trains are enabled.
|
||||
is currently unavailable when merge trains are enabled.
|
||||
|
||||
See [the related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/12267)
|
||||
for more information.
|
||||
|
||||
### Merge Train Pipeline cannot be retried
|
||||
### Merge train pipeline cannot be retried
|
||||
|
||||
When a pipeline for merge trains fails the merge request is dropped from the train and the pipeline can't be retried.
|
||||
Pipelines for merge trains run on the merged result of the changes in the merge request and
|
||||
|
@ -189,7 +192,7 @@ again, which triggers a new pipeline.
|
|||
|
||||
### Unable to add to merge train with message "The pipeline for this merge request failed."
|
||||
|
||||
Sometimes the **Start/Add to Merge Train** button is not available and the merge request says,
|
||||
Sometimes the **Start/Add to merge train** button is not available and the merge request says,
|
||||
"The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure."
|
||||
|
||||
This issue occurs when [**Pipelines must succeed**](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds)
|
||||
|
@ -197,19 +200,19 @@ is enabled in **Settings > General > Merge requests**. This option requires that
|
|||
run a new successful pipeline before you can re-add a merge request to a merge train.
|
||||
|
||||
Merge trains ensure that each pipeline has succeeded before a merge happens, so
|
||||
you can clear the **Pipelines must succeed** check box and keep
|
||||
**Enable merge trains and pipelines for merged results** (merge trains) enabled.
|
||||
you can clear the **Pipelines must succeed** checkbox and keep
|
||||
**Enable merge trains and pipelines for merged results** (merge trains) selected.
|
||||
|
||||
If you want to keep the **Pipelines must succeed** option enabled along with Merge
|
||||
Trains, create a new pipeline for merged results when this error occurs:
|
||||
If you want to keep the **Pipelines must succeed** option selected along with merge
|
||||
trains, create a new pipeline for merged results when this error occurs:
|
||||
|
||||
1. Go to the **Pipelines** tab and click **Run pipeline**.
|
||||
1. Click **Start/Add to merge train when pipeline succeeds**.
|
||||
1. On the **Pipelines** tab, select **Run pipeline**.
|
||||
1. Select **Start/Add to merge train when pipeline succeeds**.
|
||||
|
||||
See [the related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/35135)
|
||||
for more information.
|
||||
|
||||
### Merge Trains feature flag **(PREMIUM SELF)**
|
||||
### Merge trains feature flag **(PREMIUM SELF)**
|
||||
|
||||
In [GitLab 13.6 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/244831),
|
||||
you can [enable or disable merge trains in the project settings](#enable-merge-trains).
|
||||
|
@ -227,7 +230,7 @@ Feature.enable(:disable_merge_trains)
|
|||
```
|
||||
|
||||
After you enable this feature flag, all existing merge trains are cancelled and
|
||||
the **Start/Add to Merge Train** button no longer appears in merge requests.
|
||||
the **Start/Add to merge train** button no longer appears in merge requests.
|
||||
|
||||
To disable the feature flag, and enable merge trains again:
|
||||
|
||||
|
|
|
@ -304,8 +304,8 @@ Prerequisites:
|
|||
To trigger the pipeline when the upstream project is rebuilt:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > CI/CD** page.
|
||||
1. Expand the **Pipeline subscriptions** section.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **Pipeline subscriptions**.
|
||||
1. Enter the project you want to subscribe to, in the format `<namespace>/<project>`.
|
||||
For example, if the project is `https://gitlab.com/gitlab-org/gitlab`, use `gitlab-org/gitlab`.
|
||||
1. Select **Subscribe**.
|
||||
|
|
|
@ -10,7 +10,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10075) in GitLab Ultimate 12.0.
|
||||
|
||||
Use the dependency list to review your project's dependencies and key
|
||||
details about those dependencies, including their known vulnerabilities. It is a collection of dependencies in your project, including existing and new findings. To see the dependency list, go to your project and select **Security & Compliance > Dependency List**.
|
||||
details about those dependencies, including their known vulnerabilities. It is a collection of dependencies in your project, including existing and new findings.
|
||||
|
||||
To see the dependency list, go to your project and select **Security & Compliance > Dependency List**.
|
||||
|
||||
This information is sometimes referred to as a Software Bill of Materials or SBoM / BOM.
|
||||
|
||||
The dependency list only shows the results of the last successful pipeline to run on the default branch. This is why we recommend not changing the default behavior of allowing the secure jobs to fail.
|
||||
|
@ -71,4 +74,12 @@ If the [License Compliance](../../compliance/license_compliance/index.md) CI job
|
|||
## Downloading the dependency list
|
||||
|
||||
You can download your project's full list of dependencies and their details in
|
||||
`JSON` format by selecting the download button.
|
||||
`JSON` format.
|
||||
|
||||
### In the UI
|
||||
|
||||
You can download your project’s list of dependencies and their details in JSON format by selecting the **Export** button. Note that the dependency list only shows the results of the last successful pipeline to run on the default branch.
|
||||
|
||||
### Using the API
|
||||
|
||||
You can download your project’s list of dependencies [using the API](../../../api/dependencies.md#list-project-dependencies). Note this only provides the dependencies identified by the gemnasium family of analyzers and [not any other of the GitLab dependency analyzers](../dependency_scanning/analyzers.md).
|
||||
|
|
|
@ -146,6 +146,7 @@ as shown in the following table:
|
|||
| [Access to Security Dashboard](../../application_security/security_dashboard/index.md) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [Configure SAST in the UI](#configure-sast-in-the-ui) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [Customize SAST Rulesets](#customize-rulesets) | **{dotted-circle}** | **{check-circle}** |
|
||||
| [False Positive Detection](#false-positive-detection) | **{dotted-circle}** | **{check-circle}** |
|
||||
|
||||
## Contribute your scanner
|
||||
|
||||
|
@ -355,6 +356,12 @@ To create a custom ruleset:
|
|||
value = "gosec-config.json"
|
||||
```
|
||||
|
||||
### False Positive Detection **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292686) in GitLab 14.2.
|
||||
|
||||
Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard.
|
||||
|
||||
### Using CI/CD variables to pass credentials for private repositories
|
||||
|
||||
Some analyzers require downloading the project's dependencies in order to
|
||||
|
|
|
@ -100,6 +100,39 @@ The following table lists project permissions available for each role:
|
|||
| [Merge requests](project/merge_requests/index.md):<br>Manage or accept | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Manage merge approval rules (project settings) | | | | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Delete | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Reposition comments on images (posted by any user) | ✓ (*10*) | ✓ (*10*) | ✓ (*10*) | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View Requirements **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View [time tracking](project/time_tracking.md) reports | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View [wiki](project/wiki/index.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Create [snippets](snippets.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Manage labels | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View project statistics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Create, edit, delete [milestones](project/milestones/index.md). | | | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Create, edit [wiki](project/wiki/index.md) pages | | | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Enable Review Apps | | | ✓ | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>View project [Audit Events](../administration/audit_events.md) | | | ✓ (*11*) | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Add deploy keys | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Add new team members | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Change [project features visibility](../public_access/public_access.md) level | | | | ✓ (14) | ✓ |
|
||||
| [Projects](project/index.md):<br>Delete [wiki](project/wiki/index.md) pages | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Edit comments (posted by any user) | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Edit project badges | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Edit project settings | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Export project | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Manage [project access tokens](project/settings/project_access_tokens.md) **(FREE SELF)** **(PREMIUM SAAS)** (*12*) | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Manage [Project Operations](../operations/index.md) | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Share (invite) projects with groups | | | | ✓ (*8*) | ✓ (*8*) |
|
||||
| [Projects](project/index.md):<br>View 2FA status of members | | | | ✓ | ✓ |
|
||||
| [Projects](project/index.md):<br>Administer project compliance frameworks | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Archive project | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Change project visibility level | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Delete project | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Disable notification emails | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Rename project | | | | | ✓ |
|
||||
| [Projects](project/index.md):<br>Transfer project to another namespace | | | | | ✓ |
|
||||
| [Security dashboard](application_security/security_dashboard/index.md):<br>View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Security dashboard](application_security/security_dashboard/index.md):<br>Create issue from vulnerability finding **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| [Security dashboard](application_security/security_dashboard/index.md):<br>Create vulnerability from vulnerability finding **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
|
@ -110,27 +143,18 @@ The following table lists project permissions available for each role:
|
|||
| [Security dashboard](application_security/security_dashboard/index.md):<br>Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| [Security dashboard](application_security/security_dashboard/index.md):<br>View vulnerability findings in [dependency list](application_security/dependency_list/index.md) **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Manage user-starred metrics dashboards (*7*) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Reposition comments on images (posted by any user)| ✓ (*10*)| ✓ (*10*) | ✓ (*10*) | ✓ | ✓ |
|
||||
| View [Releases](project/releases/index.md) | ✓ (*6*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View a time tracking report | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View allowed and denied licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View requirements **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Archive [test case](../ci/test_cases/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Archive/reopen requirements **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create new [test case](../ci/test_cases/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit requirements **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Import/export requirements **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Move [test case](../ci/test_cases/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull [packages](packages/index.md) | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| Reopen [test case](../ci/test_cases/index.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
|
@ -138,16 +162,12 @@ The following table lists project permissions available for each role:
|
|||
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License list **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View project statistics | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Add tags | | | ✓ | ✓ | ✓ |
|
||||
| Create and edit wiki pages | | | ✓ | ✓ | ✓ |
|
||||
| Create new branches | | | ✓ | ✓ | ✓ |
|
||||
| Create or update commit status | | | ✓ (*5*) | ✓ | ✓ |
|
||||
| Create/edit/delete [releases](project/releases/index.md)| | | ✓ (*13*) | ✓ (*13*) | ✓ (*13*) |
|
||||
| Create/edit/delete a Cleanup policy | | | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
|
||||
| Enable Review Apps | | | ✓ | ✓ | ✓ |
|
||||
| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
|
||||
| Manage Feature Flags **(PREMIUM)** | | | ✓ | ✓ | ✓ |
|
||||
| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
|
||||
|
@ -158,41 +178,21 @@ The following table lists project permissions available for each role:
|
|||
| Rewrite/remove Git tags | | | ✓ | ✓ | ✓ |
|
||||
| Update a container registry | | | ✓ | ✓ | ✓ |
|
||||
| View Pods logs | | | ✓ | ✓ | ✓ |
|
||||
| View project Audit Events | | | ✓ (*11*) | ✓ | ✓ |
|
||||
| Add deploy keys to project | | | | ✓ | ✓ |
|
||||
| Add new team members | | | | ✓ | ✓ |
|
||||
| Configure project hooks | | | | ✓ | ✓ |
|
||||
| Change project features visibility level | | | | ✓ (14) | ✓ |
|
||||
| Delete [packages](packages/index.md) | | | | ✓ | ✓ |
|
||||
| Delete wiki pages | | | | ✓ | ✓ |
|
||||
| Edit comments (posted by any user) | | | | ✓ | ✓ |
|
||||
| Edit project badges | | | | ✓ | ✓ |
|
||||
| Edit project settings | | | | ✓ | ✓ |
|
||||
| Enable/disable branch protection | | | | ✓ | ✓ |
|
||||
| Enable/disable tag protections | | | | ✓ | ✓ |
|
||||
| Export project | | | | ✓ | ✓ |
|
||||
| Manage [project access tokens](project/settings/project_access_tokens.md) **(FREE SELF)** **(PREMIUM SAAS)** (*12*) | | | | ✓ | ✓ |
|
||||
| Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
|
||||
| Manage clusters | | | | ✓ | ✓ |
|
||||
| Manage Error Tracking | | | | ✓ | ✓ |
|
||||
| Manage GitLab Pages | | | | ✓ | ✓ |
|
||||
| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
|
||||
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
|
||||
| Manage Project Operations | | | | ✓ | ✓ |
|
||||
| Manage Terraform state | | | | ✓ | ✓ |
|
||||
| Push to protected branches | | | | ✓ | ✓ |
|
||||
| Remove GitLab Pages | | | | ✓ | ✓ |
|
||||
| Share (invite) projects with groups | | | | ✓ (*8*) | ✓ (*8*)|
|
||||
| Turn on/off protected branch push for developers | | | | ✓ | ✓ |
|
||||
| View 2FA status of members | | | | ✓ | ✓ |
|
||||
| Administer project compliance frameworks | | | | | ✓ |
|
||||
| Archive project | | | | | ✓ |
|
||||
| Change project visibility level | | | | | ✓ |
|
||||
| Delete project | | | | | ✓ |
|
||||
| Disable notification emails | | | | | ✓ |
|
||||
| Remove fork relationship | | | | | ✓ |
|
||||
| Rename project | | | | | ✓ |
|
||||
| Transfer project to another namespace | | | | | ✓ |
|
||||
| Force push to protected branches (*4*) | | | | | |
|
||||
| Remove protected branches (*4*) | | | | | |
|
||||
|
||||
|
|
|
@ -177,11 +177,6 @@ module Gitlab
|
|||
'unknown'
|
||||
end
|
||||
|
||||
# Monkeypatch rails with upgraded database observability
|
||||
def self.install_monkey_patches
|
||||
ActiveRecord::Base.prepend(ActiveRecordBaseTransactionMetrics)
|
||||
end
|
||||
|
||||
def self.read_only?
|
||||
false
|
||||
end
|
||||
|
@ -190,6 +185,18 @@ module Gitlab
|
|||
!read_only?
|
||||
end
|
||||
|
||||
# Monkeypatch rails with upgraded database observability
|
||||
def self.install_transaction_metrics_patches!
|
||||
ActiveRecord::Base.prepend(ActiveRecordBaseTransactionMetrics)
|
||||
end
|
||||
|
||||
def self.install_transaction_context_patches!
|
||||
ActiveRecord::ConnectionAdapters::TransactionManager
|
||||
.prepend(TransactionManagerContext)
|
||||
ActiveRecord::ConnectionAdapters::RealTransaction
|
||||
.prepend(RealTransactionContext)
|
||||
end
|
||||
|
||||
# MonkeyPatch for ActiveRecord::Base for adding observability
|
||||
module ActiveRecordBaseTransactionMetrics
|
||||
extend ActiveSupport::Concern
|
||||
|
@ -204,6 +211,32 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
module TransactionManagerContext
|
||||
def transaction_context
|
||||
@stack.first.try(:gitlab_transaction_context)
|
||||
end
|
||||
end
|
||||
|
||||
module RealTransactionContext
|
||||
def gitlab_transaction_context
|
||||
@gitlab_transaction_context ||= ::Gitlab::Database::Transaction::Context.new
|
||||
end
|
||||
|
||||
def commit
|
||||
gitlab_transaction_context.commit
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def rollback
|
||||
gitlab_transaction_context.rollback
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -127,9 +127,17 @@ module Gitlab
|
|||
# recognize the connection, this method returns the primary role
|
||||
# directly. In future, we may need to check for other sources.
|
||||
def self.db_role_for_connection(connection)
|
||||
return ROLE_PRIMARY if !enable? || proxy.blank?
|
||||
return ROLE_UNKNOWN unless connection
|
||||
|
||||
proxy.load_balancer.db_role_for_connection(connection)
|
||||
# The connection proxy does not have a role assigned
|
||||
# as this is dependent on a execution context
|
||||
return ROLE_UNKNOWN if connection.is_a?(ConnectionProxy)
|
||||
|
||||
if connection.pool.db_config.name.ends_with?(LoadBalancer::REPLICA_SUFFIX)
|
||||
ROLE_REPLICA
|
||||
else
|
||||
ROLE_PRIMARY
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,13 +8,11 @@ module Gitlab
|
|||
# hosts - The list of secondary hosts to add.
|
||||
def initialize(hosts = [])
|
||||
@hosts = hosts.shuffle
|
||||
@pools = Set.new
|
||||
@index = 0
|
||||
@mutex = Mutex.new
|
||||
@hosts_gauge = Gitlab::Metrics.gauge(:db_load_balancing_hosts, 'Current number of load balancing hosts')
|
||||
|
||||
set_metrics!
|
||||
update_pools
|
||||
end
|
||||
|
||||
def hosts
|
||||
|
@ -35,15 +33,10 @@ module Gitlab
|
|||
@mutex.synchronize { @hosts.map { |host| [host.host, host.port] } }
|
||||
end
|
||||
|
||||
def manage_pool?(pool)
|
||||
@pools.include?(pool)
|
||||
end
|
||||
|
||||
def hosts=(hosts)
|
||||
@mutex.synchronize do
|
||||
@hosts = hosts
|
||||
unsafe_shuffle
|
||||
update_pools
|
||||
end
|
||||
|
||||
set_metrics!
|
||||
|
@ -89,10 +82,6 @@ module Gitlab
|
|||
def set_metrics!
|
||||
@hosts_gauge.set({}, @hosts.length)
|
||||
end
|
||||
|
||||
def update_pools
|
||||
@pools = Set.new(@hosts.map(&:pool))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,14 +10,14 @@ module Gitlab
|
|||
class LoadBalancer
|
||||
CACHE_KEY = :gitlab_load_balancer_host
|
||||
|
||||
REPLICA_SUFFIX = '_replica'
|
||||
|
||||
attr_reader :host_list
|
||||
|
||||
# hosts - The hostnames/addresses of the additional databases.
|
||||
def initialize(hosts = [], model = ActiveRecord::Base)
|
||||
@model = model
|
||||
@host_list = HostList.new(hosts.map { |addr| Host.new(addr, self) })
|
||||
@connection_db_roles = {}.compare_by_identity
|
||||
@connection_db_roles_count = {}.compare_by_identity
|
||||
end
|
||||
|
||||
def disconnect!(timeout: 120)
|
||||
|
@ -29,7 +29,6 @@ module Gitlab
|
|||
# If no secondaries were available this method will use the primary
|
||||
# instead.
|
||||
def read(&block)
|
||||
connection = nil
|
||||
conflict_retried = 0
|
||||
|
||||
while host
|
||||
|
@ -37,12 +36,8 @@ module Gitlab
|
|||
|
||||
begin
|
||||
connection = host.connection
|
||||
track_connection_role(connection, ROLE_REPLICA)
|
||||
|
||||
return yield connection
|
||||
rescue StandardError => error
|
||||
untrack_connection_role(connection)
|
||||
|
||||
if serialization_failure?(error)
|
||||
# This error can occur when a query conflicts. See
|
||||
# https://www.postgresql.org/docs/current/static/hot-standby.html#HOT-STANDBY-CONFLICT
|
||||
|
@ -85,8 +80,6 @@ module Gitlab
|
|||
)
|
||||
|
||||
read_write(&block)
|
||||
ensure
|
||||
untrack_connection_role(connection)
|
||||
end
|
||||
|
||||
# Yields a connection that can be used for both reads and writes.
|
||||
|
@ -97,21 +90,8 @@ module Gitlab
|
|||
# a few times.
|
||||
retry_with_backoff do
|
||||
connection = pool.connection
|
||||
track_connection_role(connection, ROLE_PRIMARY)
|
||||
|
||||
yield connection
|
||||
end
|
||||
ensure
|
||||
untrack_connection_role(connection)
|
||||
end
|
||||
|
||||
# Recognize the role (primary/replica) of the database this connection
|
||||
# is connecting to. If the connection is not issued by this load
|
||||
# balancer, return nil
|
||||
def db_role_for_connection(connection)
|
||||
return @connection_db_roles[connection] if @connection_db_roles[connection]
|
||||
return ROLE_REPLICA if @host_list.manage_pool?(connection.pool)
|
||||
return ROLE_PRIMARY if connection.pool == pool
|
||||
end
|
||||
|
||||
# Returns a host to use for queries.
|
||||
|
@ -222,7 +202,7 @@ module Gitlab
|
|||
|
||||
replica_db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
|
||||
db_config.env_name,
|
||||
db_config.name + "_replica",
|
||||
db_config.name + REPLICA_SUFFIX,
|
||||
env_config
|
||||
)
|
||||
|
||||
|
@ -250,22 +230,6 @@ module Gitlab
|
|||
host.enable_query_cache! unless host.query_cache_enabled
|
||||
end
|
||||
|
||||
def track_connection_role(connection, role)
|
||||
@connection_db_roles[connection] = role
|
||||
@connection_db_roles_count[connection] ||= 0
|
||||
@connection_db_roles_count[connection] += 1
|
||||
end
|
||||
|
||||
def untrack_connection_role(connection)
|
||||
return if connection.blank? || @connection_db_roles_count[connection].blank?
|
||||
|
||||
@connection_db_roles_count[connection] -= 1
|
||||
if @connection_db_roles_count[connection] <= 0
|
||||
@connection_db_roles.delete(connection)
|
||||
@connection_db_roles_count.delete(connection)
|
||||
end
|
||||
end
|
||||
|
||||
def request_cache
|
||||
base = RequestStore[:gitlab_load_balancer] ||= {}
|
||||
base[pool] ||= {}
|
||||
|
|
125
lib/gitlab/database/transaction/context.rb
Normal file
125
lib/gitlab/database/transaction/context.rb
Normal file
|
@ -0,0 +1,125 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Transaction
|
||||
class Context
|
||||
attr_reader :context
|
||||
|
||||
LOG_DEPTH_THRESHOLD = 8
|
||||
LOG_SAVEPOINTS_THRESHOLD = 32
|
||||
LOG_DURATION_S_THRESHOLD = 300
|
||||
LOG_THROTTLE_DURATION = 1
|
||||
|
||||
def initialize
|
||||
@context = {}
|
||||
end
|
||||
|
||||
def set_start_time
|
||||
@context[:start_time] = current_timestamp
|
||||
end
|
||||
|
||||
def increment_savepoints
|
||||
@context[:savepoints] = @context[:savepoints].to_i + 1
|
||||
end
|
||||
|
||||
def increment_rollbacks
|
||||
@context[:rollbacks] = @context[:rollbacks].to_i + 1
|
||||
end
|
||||
|
||||
def increment_releases
|
||||
@context[:releases] = @context[:releases].to_i + 1
|
||||
end
|
||||
|
||||
def set_depth(depth)
|
||||
@context[:depth] = [@context[:depth].to_i, depth].max
|
||||
end
|
||||
|
||||
def track_sql(sql)
|
||||
(@context[:queries] ||= []).push(sql)
|
||||
end
|
||||
|
||||
def duration
|
||||
return unless @context[:start_time].present?
|
||||
|
||||
current_timestamp - @context[:start_time]
|
||||
end
|
||||
|
||||
def depth_threshold_exceeded?
|
||||
@context[:depth].to_i > LOG_DEPTH_THRESHOLD
|
||||
end
|
||||
|
||||
def savepoints_threshold_exceeded?
|
||||
@context[:savepoints].to_i > LOG_SAVEPOINTS_THRESHOLD
|
||||
end
|
||||
|
||||
def duration_threshold_exceeded?
|
||||
duration.to_i > LOG_DURATION_S_THRESHOLD
|
||||
end
|
||||
|
||||
def log_savepoints?
|
||||
depth_threshold_exceeded? || savepoints_threshold_exceeded?
|
||||
end
|
||||
|
||||
def log_duration?
|
||||
duration_threshold_exceeded?
|
||||
end
|
||||
|
||||
def should_log?
|
||||
!logged_already? && (log_savepoints? || log_duration?)
|
||||
end
|
||||
|
||||
def commit
|
||||
log(:commit)
|
||||
end
|
||||
|
||||
def rollback
|
||||
log(:rollback)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def queries
|
||||
@context[:queries].to_a.join("\n")
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
::Gitlab::Metrics::System.monotonic_time
|
||||
end
|
||||
|
||||
def logged_already?
|
||||
return false if @context[:last_log_timestamp].nil?
|
||||
|
||||
(current_timestamp - @context[:last_log_timestamp].to_i) < LOG_THROTTLE_DURATION
|
||||
end
|
||||
|
||||
def set_last_log_timestamp
|
||||
@context[:last_log_timestamp] = current_timestamp
|
||||
end
|
||||
|
||||
def log(operation)
|
||||
return unless should_log?
|
||||
|
||||
set_last_log_timestamp
|
||||
|
||||
attributes = {
|
||||
class: self.class.name,
|
||||
result: operation,
|
||||
duration_s: duration,
|
||||
depth: @context[:depth].to_i,
|
||||
savepoints_count: @context[:savepoints].to_i,
|
||||
rollbacks_count: @context[:rollbacks].to_i,
|
||||
releases_count: @context[:releases].to_i,
|
||||
sql: queries
|
||||
}
|
||||
|
||||
application_info(attributes)
|
||||
end
|
||||
|
||||
def application_info(attributes)
|
||||
Gitlab::AppJsonLogger.info(attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
66
lib/gitlab/database/transaction/observer.rb
Normal file
66
lib/gitlab/database/transaction/observer.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Transaction
|
||||
class Observer
|
||||
INSTRUMENTED_STATEMENTS = %w[BEGIN SAVEPOINT ROLLBACK RELEASE].freeze
|
||||
LONGEST_COMMAND_LENGTH = 'ROLLBACK TO SAVEPOINT'.length
|
||||
START_COMMENT = '/*'
|
||||
END_COMMENT = '*/'
|
||||
|
||||
def self.instrument_transactions(cmd, event)
|
||||
connection = event.payload[:connection]
|
||||
manager = connection&.transaction_manager
|
||||
return unless manager.respond_to?(:transaction_context)
|
||||
|
||||
context = manager.transaction_context
|
||||
return if context.nil?
|
||||
|
||||
if cmd.start_with?('BEGIN')
|
||||
context.set_start_time
|
||||
context.set_depth(0)
|
||||
context.track_sql(event.payload[:sql])
|
||||
elsif cmd.start_with?('SAVEPOINT ')
|
||||
context.set_depth(manager.open_transactions)
|
||||
context.increment_savepoints
|
||||
elsif cmd.start_with?('ROLLBACK TO SAVEPOINT')
|
||||
context.increment_rollbacks
|
||||
elsif cmd.start_with?('RELEASE SAVEPOINT ')
|
||||
context.increment_releases
|
||||
end
|
||||
end
|
||||
|
||||
def self.register!
|
||||
ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
|
||||
sql = event.payload.dig(:sql).to_s
|
||||
cmd = extract_sql_command(sql)
|
||||
|
||||
if cmd.start_with?(*INSTRUMENTED_STATEMENTS)
|
||||
self.instrument_transactions(cmd, event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.extract_sql_command(sql)
|
||||
return sql unless sql.start_with?(START_COMMENT)
|
||||
|
||||
index = sql.index(END_COMMENT)
|
||||
|
||||
return sql unless index
|
||||
|
||||
# /* comment */ SELECT
|
||||
#
|
||||
# We offset using a position of the end comment + 1 character to
|
||||
# accomodate a space between Marginalia comment and a SQL statement.
|
||||
offset = index + END_COMMENT.length + 1
|
||||
|
||||
# Avoid duplicating the entire string. This isn't optimized to
|
||||
# strip extra spaces, but we assume that this doesn't happen
|
||||
# for performance reasons.
|
||||
sql[offset..offset + LONGEST_COMMAND_LENGTH]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ require 'gitlab/email/handler/reply_processing'
|
|||
|
||||
# handles note/reply creation emails with these formats:
|
||||
# incoming+1234567890abcdef1234567890abcdef@incoming.gitlab.com
|
||||
# Quoted material is _not_ stripped but appended as a `details` section
|
||||
module Gitlab
|
||||
module Email
|
||||
module Handler
|
||||
|
@ -24,7 +25,7 @@ module Gitlab
|
|||
validate_permission!(:create_note)
|
||||
|
||||
raise NoteableNotFoundError unless noteable
|
||||
raise EmptyEmailError if message.blank?
|
||||
raise EmptyEmailError if note_message.blank?
|
||||
|
||||
verify_record!(
|
||||
record: create_note,
|
||||
|
@ -47,7 +48,13 @@ module Gitlab
|
|||
end
|
||||
|
||||
def create_note
|
||||
sent_notification.create_reply(message)
|
||||
sent_notification.create_reply(note_message)
|
||||
end
|
||||
|
||||
def note_message
|
||||
return message unless sent_notification.noteable_type == "Issue"
|
||||
|
||||
message_with_appended_reply
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,13 +35,20 @@ module Gitlab
|
|||
@message_with_reply ||= process_message(trim_reply: false)
|
||||
end
|
||||
|
||||
def process_message(**kwargs)
|
||||
message = ReplyParser.new(mail, **kwargs).execute.strip
|
||||
message_with_attachments = add_attachments(message)
|
||||
def message_with_appended_reply
|
||||
@message_with_appended_reply ||= process_message(append_reply: true)
|
||||
end
|
||||
|
||||
# Support bot is specifically forbidden
|
||||
# from using slash commands.
|
||||
strip_quick_actions(message_with_attachments)
|
||||
def process_message(**kwargs)
|
||||
message, stripped_text = ReplyParser.new(mail, **kwargs).execute
|
||||
message = message.strip
|
||||
|
||||
message_with_attachments = add_attachments(message)
|
||||
# Support bot is specifically forbidden from using slash commands.
|
||||
message = strip_quick_actions(message_with_attachments)
|
||||
return message unless kwargs[:append_reply]
|
||||
|
||||
append_reply(message, stripped_text)
|
||||
end
|
||||
|
||||
def add_attachments(reply)
|
||||
|
@ -92,10 +99,22 @@ module Gitlab
|
|||
def strip_quick_actions(content)
|
||||
return content unless author.support_bot?
|
||||
|
||||
command_definitions = ::QuickActions::InterpretService.command_definitions
|
||||
extractor = ::Gitlab::QuickActions::Extractor.new(command_definitions)
|
||||
quick_actions_extractor.redact_commands(content)
|
||||
end
|
||||
|
||||
extractor.redact_commands(content)
|
||||
def quick_actions_extractor
|
||||
command_definitions = ::QuickActions::InterpretService.command_definitions
|
||||
::Gitlab::QuickActions::Extractor.new(command_definitions)
|
||||
end
|
||||
|
||||
def append_reply(message, reply)
|
||||
return message if message.blank? || reply.blank?
|
||||
|
||||
# Do not append if message only contains slash commands
|
||||
body, _commands = quick_actions_extractor.extract_commands(message)
|
||||
return message if body.empty?
|
||||
|
||||
message + "\n\n<details><summary>...</summary>\n\n#{reply}\n\n</details>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,20 +6,17 @@ module Gitlab
|
|||
class ReplyParser
|
||||
attr_accessor :message
|
||||
|
||||
def initialize(message, trim_reply: true)
|
||||
def initialize(message, trim_reply: true, append_reply: false)
|
||||
@message = message
|
||||
@trim_reply = trim_reply
|
||||
@append_reply = append_reply
|
||||
end
|
||||
|
||||
def execute
|
||||
body = select_body(message)
|
||||
|
||||
encoding = body.encoding
|
||||
|
||||
if @trim_reply
|
||||
body = EmailReplyTrimmer.trim(body)
|
||||
end
|
||||
|
||||
body, stripped_text = EmailReplyTrimmer.trim(body, @append_reply) if @trim_reply
|
||||
return '' unless body
|
||||
|
||||
# not using /\s+$/ here because that deletes empty lines
|
||||
|
@ -30,7 +27,10 @@ module Gitlab
|
|||
# so we detect it manually here.
|
||||
return "" if body.lines.all? { |l| l.strip.empty? || l.start_with?('>') }
|
||||
|
||||
body.force_encoding(encoding).encode("UTF-8")
|
||||
encoded_body = body.force_encoding(encoding).encode("UTF-8")
|
||||
return encoded_body unless @append_reply
|
||||
|
||||
[encoded_body, stripped_text.force_encoding(encoding).encode("UTF-8")]
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -12,6 +12,7 @@ module Gitlab
|
|||
included do
|
||||
scope :public_only, -> { where(visibility_level: PUBLIC) }
|
||||
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
|
||||
scope :private_only, -> { where(visibility_level: PRIVATE) }
|
||||
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
|
||||
|
||||
scope :public_to_user, -> (user = nil) do
|
||||
|
|
23
spec/fixtures/emails/no_content_with_quote.eml
vendored
Normal file
23
spec/fixtures/emails/no_content_with_quote.eml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
Return-Path: <jake@adventuretime.ooo>
|
||||
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||
From: Jake the Dog <jake@adventuretime.ooo>
|
||||
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
|
||||
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/plain;
|
||||
charset=ISO-8859-1
|
||||
Content-Transfer-Encoding: 7bit
|
||||
X-Sieve: CMU Sieve 2.2
|
||||
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||
|
||||
--
|
||||
> quote line 1
|
||||
> quote line 2
|
||||
> quote line 3
|
|
@ -56,44 +56,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#manage_pool?' do
|
||||
context 'when the testing pool belongs to one host of the host list' do
|
||||
it 'returns true' do
|
||||
pool = host_list.hosts.first.pool
|
||||
|
||||
expect(host_list.manage_pool?(pool)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the testing pool belongs to a former host of the host list' do
|
||||
it 'returns false' do
|
||||
pool = host_list.hosts.first.pool
|
||||
host_list.hosts = [
|
||||
Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
|
||||
]
|
||||
|
||||
expect(host_list.manage_pool?(pool)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the testing pool belongs to a new host of the host list' do
|
||||
it 'returns true' do
|
||||
host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
|
||||
host_list.hosts = [host]
|
||||
|
||||
expect(host_list.manage_pool?(host.pool)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the testing pool does not have any relation with the host list' do
|
||||
it 'returns false' do
|
||||
host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
|
||||
|
||||
expect(host_list.manage_pool?(host.pool)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hosts' do
|
||||
it 'returns a copy of the host' do
|
||||
first = host_list.hosts
|
||||
|
|
|
@ -137,126 +137,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#db_role_for_connection' do
|
||||
context 'when the load balancer creates the connection with #read' do
|
||||
it 'returns :replica' do
|
||||
role = nil
|
||||
lb.read do |connection|
|
||||
role = lb.db_role_for_connection(connection)
|
||||
end
|
||||
|
||||
expect(role).to be(:replica)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer uses nested #read' do
|
||||
it 'returns :replica' do
|
||||
roles = []
|
||||
lb.read do |connection_1|
|
||||
lb.read do |connection_2|
|
||||
roles << lb.db_role_for_connection(connection_2)
|
||||
end
|
||||
roles << lb.db_role_for_connection(connection_1)
|
||||
end
|
||||
|
||||
expect(roles).to eq([:replica, :replica])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer creates the connection with #read_write' do
|
||||
it 'returns :primary' do
|
||||
role = nil
|
||||
lb.read_write do |connection|
|
||||
role = lb.db_role_for_connection(connection)
|
||||
end
|
||||
|
||||
expect(role).to be(:primary)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer uses nested #read_write' do
|
||||
it 'returns :primary' do
|
||||
roles = []
|
||||
lb.read_write do |connection_1|
|
||||
lb.read_write do |connection_2|
|
||||
roles << lb.db_role_for_connection(connection_2)
|
||||
end
|
||||
roles << lb.db_role_for_connection(connection_1)
|
||||
end
|
||||
|
||||
expect(roles).to eq([:primary, :primary])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer falls back the connection creation to primary' do
|
||||
it 'returns :primary' do
|
||||
allow(lb).to receive(:serialization_failure?).and_return(true)
|
||||
|
||||
role = nil
|
||||
raised = 7 # 2 hosts = 6 retries
|
||||
|
||||
lb.read do |connection|
|
||||
if raised > 0
|
||||
raised -= 1
|
||||
raise
|
||||
end
|
||||
|
||||
role = lb.db_role_for_connection(connection)
|
||||
end
|
||||
|
||||
expect(role).to be(:primary)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer uses replica after recovery from a failure' do
|
||||
it 'returns :replica' do
|
||||
allow(lb).to receive(:connection_error?).and_return(true)
|
||||
|
||||
role = nil
|
||||
raised = false
|
||||
|
||||
lb.read do |connection|
|
||||
unless raised
|
||||
raised = true
|
||||
raise
|
||||
end
|
||||
|
||||
role = lb.db_role_for_connection(connection)
|
||||
end
|
||||
|
||||
expect(role).to be(:replica)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the connection comes from a pool managed by the host list' do
|
||||
it 'returns :replica' do
|
||||
connection = double(:connection)
|
||||
allow(connection).to receive(:pool).and_return(lb.host_list.hosts.first.pool)
|
||||
|
||||
expect(lb.db_role_for_connection(connection)).to be(:replica)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the connection comes from the primary pool' do
|
||||
it 'returns :primary' do
|
||||
connection = double(:connection)
|
||||
allow(connection).to receive(:pool).and_return(lb.send(:pool))
|
||||
|
||||
expect(lb.db_role_for_connection(connection)).to be(:primary)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the connection does not come from any known pool' do
|
||||
it 'returns nil' do
|
||||
connection = double(:connection)
|
||||
pool = double(:connection_pool)
|
||||
allow(connection).to receive(:pool).and_return(pool)
|
||||
|
||||
expect(lb.db_role_for_connection(connection)).to be(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#host' do
|
||||
it 'returns the secondary host to use' do
|
||||
expect(lb.host).to be_an_instance_of(Gitlab::Database::LoadBalancing::Host)
|
||||
|
|
|
@ -296,55 +296,37 @@ RSpec.describe Gitlab::Database::LoadBalancing do
|
|||
end
|
||||
|
||||
describe '.db_role_for_connection' do
|
||||
let(:connection) { double(:conneciton) }
|
||||
|
||||
context 'when the load balancing is not configured' do
|
||||
before do
|
||||
allow(described_class).to receive(:enable?).and_return(false)
|
||||
end
|
||||
let(:connection) { ActiveRecord::Base.connection }
|
||||
|
||||
it 'returns primary' do
|
||||
expect(described_class.db_role_for_connection(connection)).to be(:primary)
|
||||
expect(described_class.db_role_for_connection(connection)).to eq(:primary)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancing is configured' do
|
||||
let(:proxy) { described_class::ConnectionProxy.new(%w(foo)) }
|
||||
let(:load_balancer) { described_class::LoadBalancer.new(%w(foo)) }
|
||||
let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host }
|
||||
let(:proxy) { described_class::ConnectionProxy.new([db_host]) }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:enable?).and_return(true)
|
||||
allow(described_class).to receive(:proxy).and_return(proxy)
|
||||
allow(proxy).to receive(:load_balancer).and_return(load_balancer)
|
||||
context 'when a proxy connection is used' do
|
||||
it 'returns :unknown' do
|
||||
expect(described_class.db_role_for_connection(proxy)).to eq(:unknown)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer returns :replica' do
|
||||
context 'when a read connection is used' do
|
||||
it 'returns :replica' do
|
||||
allow(load_balancer).to receive(:db_role_for_connection).and_return(:replica)
|
||||
|
||||
expect(described_class.db_role_for_connection(connection)).to be(:replica)
|
||||
|
||||
expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
|
||||
proxy.load_balancer.read do |connection|
|
||||
expect(described_class.db_role_for_connection(connection)).to eq(:replica)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer returns :primary' do
|
||||
context 'when a read_write connection is used' do
|
||||
it 'returns :primary' do
|
||||
allow(load_balancer).to receive(:db_role_for_connection).and_return(:primary)
|
||||
|
||||
expect(described_class.db_role_for_connection(connection)).to be(:primary)
|
||||
|
||||
expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the load balancer returns nil' do
|
||||
it 'returns nil' do
|
||||
allow(load_balancer).to receive(:db_role_for_connection).and_return(nil)
|
||||
|
||||
expect(described_class.db_role_for_connection(connection)).to be(nil)
|
||||
|
||||
expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
|
||||
proxy.load_balancer.read_write do |connection|
|
||||
expect(described_class.db_role_for_connection(connection)).to eq(:primary)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
144
spec/lib/gitlab/database/transaction/context_spec.rb
Normal file
144
spec/lib/gitlab/database/transaction/context_spec.rb
Normal file
|
@ -0,0 +1,144 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Transaction::Context do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:data) { subject.context }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::LOG_THROTTLE", 100)
|
||||
end
|
||||
|
||||
describe '#set_start_time' do
|
||||
before do
|
||||
subject.set_start_time
|
||||
end
|
||||
|
||||
it 'sets start_time' do
|
||||
expect(data).to have_key(:start_time)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#increment_savepoints' do
|
||||
before do
|
||||
2.times { subject.increment_savepoints }
|
||||
end
|
||||
|
||||
it { expect(data[:savepoints]).to eq(2) }
|
||||
end
|
||||
|
||||
describe '#increment_rollbacks' do
|
||||
before do
|
||||
3.times { subject.increment_rollbacks }
|
||||
end
|
||||
|
||||
it { expect(data[:rollbacks]).to eq(3) }
|
||||
end
|
||||
|
||||
describe '#increment_releases' do
|
||||
before do
|
||||
4.times { subject.increment_releases }
|
||||
end
|
||||
|
||||
it { expect(data[:releases]).to eq(4) }
|
||||
end
|
||||
|
||||
describe '#set_depth' do
|
||||
before do
|
||||
subject.set_depth(2)
|
||||
end
|
||||
|
||||
it { expect(data[:depth]).to eq(2) }
|
||||
end
|
||||
|
||||
describe '#track_sql' do
|
||||
before do
|
||||
subject.track_sql('SELECT 1')
|
||||
subject.track_sql('SELECT * FROM users')
|
||||
end
|
||||
|
||||
it { expect(data[:queries]).to eq(['SELECT 1', 'SELECT * FROM users']) }
|
||||
end
|
||||
|
||||
describe '#duration' do
|
||||
before do
|
||||
subject.set_start_time
|
||||
end
|
||||
|
||||
it { expect(subject.duration).to be >= 0 }
|
||||
end
|
||||
|
||||
context 'when depth is low' do
|
||||
it 'does not log data upon COMMIT' do
|
||||
expect(subject).not_to receive(:application_info)
|
||||
|
||||
subject.commit
|
||||
end
|
||||
|
||||
it 'does not log data upon ROLLBACK' do
|
||||
expect(subject).not_to receive(:application_info)
|
||||
|
||||
subject.rollback
|
||||
end
|
||||
|
||||
it '#should_log? returns false' do
|
||||
expect(subject.should_log?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'logs transaction data' do
|
||||
it 'logs once upon COMMIT' do
|
||||
expect(subject).to receive(:application_info).and_call_original
|
||||
|
||||
2.times { subject.commit }
|
||||
end
|
||||
|
||||
it 'logs once upon ROLLBACK' do
|
||||
expect(subject).to receive(:application_info).once
|
||||
|
||||
2.times { subject.rollback }
|
||||
end
|
||||
|
||||
it 'logs again when log throttle duration passes' do
|
||||
expect(subject).to receive(:application_info).twice.and_call_original
|
||||
|
||||
2.times { subject.commit }
|
||||
|
||||
data[:last_log_timestamp] -= (described_class::LOG_THROTTLE_DURATION + 1)
|
||||
|
||||
subject.commit
|
||||
end
|
||||
|
||||
it '#should_log? returns true' do
|
||||
expect(subject.should_log?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when depth exceeds threshold' do
|
||||
before do
|
||||
subject.set_depth(described_class::LOG_DEPTH_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it_behaves_like 'logs transaction data'
|
||||
end
|
||||
|
||||
context 'when savepoints count exceeds threshold' do
|
||||
before do
|
||||
data[:savepoints] = described_class::LOG_SAVEPOINTS_THRESHOLD + 1
|
||||
end
|
||||
|
||||
it_behaves_like 'logs transaction data'
|
||||
end
|
||||
|
||||
context 'when duration exceeds threshold' do
|
||||
before do
|
||||
subject.set_start_time
|
||||
|
||||
data[:start_time] -= (described_class::LOG_DURATION_S_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
it_behaves_like 'logs transaction data'
|
||||
end
|
||||
end
|
57
spec/lib/gitlab/database/transaction/observer_spec.rb
Normal file
57
spec/lib/gitlab/database/transaction/observer_spec.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Transaction::Observer do
|
||||
# Use the delete DB strategy so that the test won't be wrapped in a transaction
|
||||
describe '.instrument_transactions', :delete do
|
||||
let(:transaction_context) { ActiveRecord::Base.connection.transaction_manager.transaction_context }
|
||||
let(:context) { transaction_context.context }
|
||||
|
||||
around do |example|
|
||||
# Emulate production environment when SQL comments come first to avoid truncation
|
||||
Marginalia::Comment.prepend_comment = true
|
||||
subscriber = described_class.register!
|
||||
|
||||
example.run
|
||||
|
||||
ActiveSupport::Notifications.unsubscribe(subscriber)
|
||||
Marginalia::Comment.prepend_comment = false
|
||||
end
|
||||
|
||||
it 'tracks transaction data', :aggregate_failures do
|
||||
ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.transaction(requires_new: true) do
|
||||
User.first
|
||||
|
||||
expect(transaction_context).to be_a(::Gitlab::Database::Transaction::Context)
|
||||
expect(context.keys).to match_array(%i(start_time depth savepoints queries))
|
||||
expect(context[:depth]).to eq(2)
|
||||
expect(context[:savepoints]).to eq(1)
|
||||
expect(context[:queries].length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
expect(context[:depth]).to eq(2)
|
||||
expect(context[:savepoints]).to eq(1)
|
||||
expect(context[:releases]).to eq(1)
|
||||
end
|
||||
|
||||
describe '.extract_sql_command' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:sql, :expected) do
|
||||
'SELECT 1' | 'SELECT 1'
|
||||
'/* test comment */ SELECT 1' | 'SELECT 1'
|
||||
'/* test comment */ ROLLBACK TO SAVEPOINT point1' | 'ROLLBACK TO SAVEPOINT '
|
||||
'SELECT 1 /* trailing comment */' | 'SELECT 1 /* trailing comment */'
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
expect(described_class.extract_sql_command(sql)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -110,6 +110,60 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when email contains reply' do
|
||||
shared_examples 'no content message' do
|
||||
context 'when email contains quoted text only' do
|
||||
let(:email_raw) { fixture_file('emails/no_content_with_quote.eml') }
|
||||
|
||||
it 'raises an EmptyEmailError' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email contains quoted text and quick commands only' do
|
||||
let(:email_raw) { fixture_file('emails/commands_only_reply.eml') }
|
||||
|
||||
it 'does not create a discussion' do
|
||||
expect { receiver.execute }.not_to change { noteable.notes.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when noteable is not an issue' do
|
||||
let_it_be(:note) { create(:note_on_merge_request, project: project) }
|
||||
|
||||
it_behaves_like 'no content message'
|
||||
|
||||
context 'when email contains text, quoted text and quick commands' do
|
||||
let(:email_raw) { fixture_file('emails/commands_in_reply.eml') }
|
||||
|
||||
it 'creates a discussion without appended reply' do
|
||||
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||
new_note = noteable.notes.last
|
||||
|
||||
expect(new_note.note).not_to include('<details><summary>...</summary>')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when noteable is an issue' do
|
||||
let_it_be(:note) { create(:note_on_issue, project: project) }
|
||||
|
||||
it_behaves_like 'no content message'
|
||||
|
||||
context 'when email contains text, quoted text and quick commands' do
|
||||
let(:email_raw) { fixture_file('emails/commands_in_reply.eml') }
|
||||
|
||||
it 'creates a discussion with appended reply' do
|
||||
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||
new_note = noteable.notes.last
|
||||
|
||||
expect(new_note.note).to include('<details><summary>...</summary>')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when note is not a discussion' do
|
||||
let(:note) { create(:note_on_merge_request, project: project) }
|
||||
|
||||
|
|
|
@ -228,5 +228,21 @@ RSpec.describe Gitlab::Email::ReplyParser do
|
|||
BODY
|
||||
)
|
||||
end
|
||||
|
||||
it "appends trimmed reply when when append_reply option is true" do
|
||||
body = <<-BODY.strip_heredoc.chomp
|
||||
The reply by email functionality should be extended to allow creating a new issue by email.
|
||||
even when the email is forwarded to the project which may include lines that begin with ">"
|
||||
|
||||
there should be a quote below this line:
|
||||
BODY
|
||||
|
||||
reply = <<-BODY.strip_heredoc.chomp
|
||||
> this is a quote
|
||||
BODY
|
||||
|
||||
expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { append_reply: true }))
|
||||
.to contain_exactly(body, reply)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -640,6 +640,12 @@ RSpec.describe Group do
|
|||
it { is_expected.to match_array([private_group, internal_group]) }
|
||||
end
|
||||
|
||||
describe 'private_only' do
|
||||
subject { described_class.private_only.to_a }
|
||||
|
||||
it { is_expected.to match_array([private_group]) }
|
||||
end
|
||||
|
||||
describe 'with_onboarding_progress' do
|
||||
subject { described_class.with_onboarding_progress }
|
||||
|
||||
|
|
Loading…
Reference in a new issue