Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
37a0f5e2cf
commit
282e71d660
|
@ -13,6 +13,7 @@ class Environment < ApplicationRecord
|
||||||
self.reactive_cache_work_type = :external_dependency
|
self.reactive_cache_work_type = :external_dependency
|
||||||
|
|
||||||
belongs_to :project, optional: false
|
belongs_to :project, optional: false
|
||||||
|
belongs_to :merge_request, optional: true
|
||||||
|
|
||||||
use_fast_destroy :all_deployments
|
use_fast_destroy :all_deployments
|
||||||
nullify_if_blank :external_url
|
nullify_if_blank :external_url
|
||||||
|
@ -68,6 +69,7 @@ class Environment < ApplicationRecord
|
||||||
allow_nil: true
|
allow_nil: true
|
||||||
|
|
||||||
validate :safe_external_url
|
validate :safe_external_url
|
||||||
|
validate :merge_request_not_changed
|
||||||
|
|
||||||
delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true
|
delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true
|
||||||
delegate :auto_rollback_enabled?, to: :project
|
delegate :auto_rollback_enabled?, to: :project
|
||||||
|
@ -524,6 +526,12 @@ class Environment < ApplicationRecord
|
||||||
self.tier ||= guess_tier
|
self.tier ||= guess_tier
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_request_not_changed
|
||||||
|
if merge_request_id_changed? && persisted?
|
||||||
|
errors.add(:merge_request, 'merge_request cannot be changed')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Guessing the tier of the environment if it's not explicitly specified by users.
|
# Guessing the tier of the environment if it's not explicitly specified by users.
|
||||||
# See https://en.wikipedia.org/wiki/Deployment_environment for industry standard deployment environments
|
# See https://en.wikipedia.org/wiki/Deployment_environment for industry standard deployment environments
|
||||||
def guess_tier
|
def guess_tier
|
||||||
|
|
|
@ -116,6 +116,7 @@ class MergeRequest < ApplicationRecord
|
||||||
|
|
||||||
has_many :draft_notes
|
has_many :draft_notes
|
||||||
has_many :reviews, inverse_of: :merge_request
|
has_many :reviews, inverse_of: :merge_request
|
||||||
|
has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request
|
||||||
|
|
||||||
KNOWN_MERGE_PARAMS = [
|
KNOWN_MERGE_PARAMS = [
|
||||||
:auto_merge_strategy,
|
:auto_merge_strategy,
|
||||||
|
|
|
@ -25,8 +25,19 @@ module Environments
|
||||||
def execute_for_merge_request_pipeline(merge_request)
|
def execute_for_merge_request_pipeline(merge_request)
|
||||||
return unless merge_request.actual_head_pipeline&.merge_request?
|
return unless merge_request.actual_head_pipeline&.merge_request?
|
||||||
|
|
||||||
merge_request.environments_in_head_pipeline(deployment_status: :success).each do |environment|
|
created_environments = merge_request.created_environments
|
||||||
execute(environment)
|
|
||||||
|
if created_environments.any?
|
||||||
|
created_environments.each { |env| execute(env) }
|
||||||
|
else
|
||||||
|
environments_in_head_pipeline = merge_request.environments_in_head_pipeline(deployment_status: :success)
|
||||||
|
environments_in_head_pipeline.each { |env| execute(env) }
|
||||||
|
|
||||||
|
if environments_in_head_pipeline.any?
|
||||||
|
# If we don't see a message often, we'd be able to remove this path. (or likely in GitLab 16.0)
|
||||||
|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/372965
|
||||||
|
Gitlab::AppJsonLogger.info(message: 'Running legacy dynamic environment stop logic', project_id: project.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
%span.gl-text-truncate.gl-sm-ml-3
|
%span.gl-text-truncate.gl-sm-ml-3
|
||||||
= key.fingerprint
|
= key.fingerprint
|
||||||
|
|
||||||
.gl-mt-3= s_('Profiles|Created%{time_ago}'.html_safe) % { time_ago: time_ago_with_tooltip(key.created_at, html_class: 'gl-ml-2')}
|
.gl-mt-3= html_escape(s_('Profiles|Created%{time_ago}')) % { time_ago: time_ago_with_tooltip(key.created_at, html_class: 'gl-ml-2').html_safe}
|
||||||
|
|
||||||
.key-list-item-dates
|
.key-list-item-dates
|
||||||
%span.last-used-at.gl-mr-3
|
%span.last-used-at.gl-mr-3
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddMergeRequestIdToEnvironments < Gitlab::Database::Migration[2.0]
|
||||||
|
def change
|
||||||
|
add_column :environments, :merge_request_id, :bigint
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IndexEvironmentsOnMergeRequestId < Gitlab::Database::Migration[2.0]
|
||||||
|
INDEX_NAME = 'index_environments_on_merge_request_id'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :environments, :merge_request_id, name: INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :environments, INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddFkConstraintToEnvironmentsMergeRequestId < Gitlab::Database::Migration[2.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_foreign_key :environments, :merge_requests, column: :merge_request_id, on_delete: :nullify
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_foreign_key_if_exists :environments, column: :merge_request_id
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
3e29afa3670370b8f5801523711d0689f1228a880b1941c44798f4bc76bedbb0
|
|
@ -0,0 +1 @@
|
||||||
|
b29e850775a327dcf6e37e25a43066a0638a55a4e0bd6b818cf496f0b97c6f82
|
|
@ -0,0 +1 @@
|
||||||
|
5b1c25848e3e890fe27c3a43effce093af5f0fe42118c7976919acef84387a0a
|
|
@ -15036,7 +15036,8 @@ CREATE TABLE environments (
|
||||||
slug character varying NOT NULL,
|
slug character varying NOT NULL,
|
||||||
auto_stop_at timestamp with time zone,
|
auto_stop_at timestamp with time zone,
|
||||||
auto_delete_at timestamp with time zone,
|
auto_delete_at timestamp with time zone,
|
||||||
tier smallint
|
tier smallint,
|
||||||
|
merge_request_id bigint
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE SEQUENCE environments_id_seq
|
CREATE SEQUENCE environments_id_seq
|
||||||
|
@ -28625,6 +28626,8 @@ CREATE INDEX index_emails_on_user_id ON emails USING btree (user_id);
|
||||||
|
|
||||||
CREATE INDEX index_enabled_clusters_on_id ON clusters USING btree (id) WHERE (enabled = true);
|
CREATE INDEX index_enabled_clusters_on_id ON clusters USING btree (id) WHERE (enabled = true);
|
||||||
|
|
||||||
|
CREATE INDEX index_environments_on_merge_request_id ON environments USING btree (merge_request_id);
|
||||||
|
|
||||||
CREATE INDEX index_environments_on_name_varchar_pattern_ops ON environments USING btree (name varchar_pattern_ops);
|
CREATE INDEX index_environments_on_name_varchar_pattern_ops ON environments USING btree (name varchar_pattern_ops);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_environments_on_project_id_and_name ON environments USING btree (project_id, name);
|
CREATE UNIQUE INDEX index_environments_on_project_id_and_name ON environments USING btree (project_id, name);
|
||||||
|
@ -32273,6 +32276,9 @@ ALTER TABLE ONLY deployments
|
||||||
ALTER TABLE ONLY epics
|
ALTER TABLE ONLY epics
|
||||||
ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
|
ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY environments
|
||||||
|
ADD CONSTRAINT fk_01a033a308 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
ALTER TABLE ONLY incident_management_escalation_rules
|
ALTER TABLE ONLY incident_management_escalation_rules
|
||||||
ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,7 @@ The following are required to run Geo:
|
||||||
- Git 2.9 or later
|
- Git 2.9 or later
|
||||||
- Git-lfs 2.4.2 or later on the user side when using LFS
|
- Git-lfs 2.4.2 or later on the user side when using LFS
|
||||||
- All sites must run [the same GitLab and PostgreSQL versions](setup/database.md#postgresql-replication).
|
- All sites must run [the same GitLab and PostgreSQL versions](setup/database.md#postgresql-replication).
|
||||||
|
- If using different operating system versions between Geo sites, [check OS locale data compatibility](replication/troubleshooting.md#check-os-locale-data-compatibility) across Geo sites.
|
||||||
|
|
||||||
Additionally, check the GitLab [minimum requirements](../../install/requirements.md),
|
Additionally, check the GitLab [minimum requirements](../../install/requirements.md),
|
||||||
and we recommend you use the latest version of GitLab for a better experience.
|
and we recommend you use the latest version of GitLab for a better experience.
|
||||||
|
|
|
@ -1234,3 +1234,33 @@ If the above steps are **not successful**, proceed through the next steps:
|
||||||
## Additional tools
|
## Additional tools
|
||||||
|
|
||||||
There are useful snippets for manipulating Geo internals in the [GitLab Rails Cheat Sheet](../../troubleshooting/gitlab_rails_cheat_sheet.md#geo). For example, you can find how to manually sync or verify a replicable in Rails console.
|
There are useful snippets for manipulating Geo internals in the [GitLab Rails Cheat Sheet](../../troubleshooting/gitlab_rails_cheat_sheet.md#geo). For example, you can find how to manually sync or verify a replicable in Rails console.
|
||||||
|
|
||||||
|
## Check OS locale data compatibility
|
||||||
|
|
||||||
|
If different operating systems or different operating system versions are deployed across Geo sites, we recommend that you perform a locale data compatibility check setting up Geo.
|
||||||
|
|
||||||
|
Geo uses PostgreSQL and Streaming Replication to replicate data across Geo sites. PostgreSQL uses locale data provided by the operating system’s C library for sorting text. If the locale data in the C library is incompatible across Geo sites, erroneous query results that lead to [incorrect behavior on secondary sites](https://gitlab.com/gitlab-org/gitlab/-/issues/360723). See [here](https://wiki.postgresql.org/wiki/Locale_data_changes) for more details.
|
||||||
|
|
||||||
|
On all hosts running PostgreSQL, across all Geo sites, run the following shell command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
( echo "1-1"; echo "11" ) | LC_COLLATE=en_US.UTF-8 sort
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will either look like:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
1-1
|
||||||
|
11
|
||||||
|
```
|
||||||
|
|
||||||
|
or the reverse order:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
11
|
||||||
|
1-1
|
||||||
|
```
|
||||||
|
|
||||||
|
If the output is identical on all hosts, then they running compatible versions of locale data.
|
||||||
|
|
||||||
|
If the output differs on some hosts, then PostgreSQL replication will not work properly. We advise that you select operating system versions that are compatible.
|
||||||
|
|
|
@ -648,7 +648,7 @@ installation, run the following in the [GitLab Rails console](operations/rails_c
|
||||||
Plan.default.actual_limits.update!(ci_max_artifact_size_junit: 10)
|
Plan.default.actual_limits.update!(ci_max_artifact_size_junit: 10)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Number of files per GitLab Pages web-site
|
### Number of files per GitLab Pages website
|
||||||
|
|
||||||
The total number of file entries (including directories and symlinks) is limited to `200,000` per
|
The total number of file entries (including directories and symlinks) is limited to `200,000` per
|
||||||
GitLab Pages website.
|
GitLab Pages website.
|
||||||
|
@ -663,6 +663,14 @@ For example, to change the limit to `100`:
|
||||||
Plan.default.actual_limits.update!(pages_file_entries: 100)
|
Plan.default.actual_limits.update!(pages_file_entries: 100)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Number of custom domains per GitLab Pages website
|
||||||
|
|
||||||
|
The total number of custom domains per GitLab Pages website is limited to `150` for [GitLab SaaS](../subscriptions/gitlab_com/index.md).
|
||||||
|
|
||||||
|
The default limit for [GitLab self-managed](../subscriptions/self_managed/index.md) is `0` (unlimited).
|
||||||
|
To set a limit on your self-managed instance, use the
|
||||||
|
[Admin Area](pages/index.md#set-maximum-number-of-gitlab-pages-custom-domains-for-a-project).
|
||||||
|
|
||||||
### Number of registered runners per scope
|
### Number of registered runners per scope
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12. Disabled by default.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12. Disabled by default.
|
||||||
|
|
|
@ -1113,6 +1113,19 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
|
||||||
--form "avatar=@/tmp/example.png"
|
--form "avatar=@/tmp/example.png"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Remove a group avatar
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96421) in GitLab 15.4.
|
||||||
|
|
||||||
|
To remove a group avatar, use a blank value for the `avatar` attribute.
|
||||||
|
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22" \
|
||||||
|
--data "avatar="
|
||||||
|
```
|
||||||
|
|
||||||
## Remove group
|
## Remove group
|
||||||
|
|
||||||
> - Immediately deleting subgroups was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360008) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `immediate_delete_subgroup_api`. Disabled by default.
|
> - Immediately deleting subgroups was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360008) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `immediate_delete_subgroup_api`. Disabled by default.
|
||||||
|
|
|
@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
## List project repository tags
|
## List project repository tags
|
||||||
|
|
||||||
|
> `version` value for the `order_by` attribute [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95150) in GitLab 15.4.
|
||||||
|
|
||||||
Get a list of repository tags from a project, sorted by update date and time in descending order. This endpoint can be accessed without authentication if the
|
Get a list of repository tags from a project, sorted by update date and time in descending order. This endpoint can be accessed without authentication if the
|
||||||
repository is publicly accessible.
|
repository is publicly accessible.
|
||||||
|
|
||||||
|
@ -19,9 +21,9 @@ Parameters:
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `id` | integer/string| yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user|
|
| `id` | integer or string| yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||||
| `order_by` | string | no | Return tags ordered by `name`, `updated`, or `version` (since 15.4) fields. Default is `updated` |
|
| `order_by` | string | no | Return tags ordered by `name`, `updated`, or `version`. Default is `updated`. |
|
||||||
| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` |
|
| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc`. |
|
||||||
| `search` | string | no | Return list of tags matching the search criteria. You can use `^term` and `term$` to find tags that begin and end with `term` respectively. No other regular expressions are supported. |
|
| `search` | string | no | Return list of tags matching the search criteria. You can use `^term` and `term$` to find tags that begin and end with `term` respectively. No other regular expressions are supported. |
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -115,7 +117,7 @@ Parameters:
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| --------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||||
| `tag_name` | string | yes | The name of a tag |
|
| `tag_name` | string | yes | The name of a tag |
|
||||||
| `ref` | string | yes | Create tag using commit SHA, another tag name, or branch name |
|
| `ref` | string | yes | Create tag using commit SHA, another tag name, or branch name |
|
||||||
| `message` | string | no | Creates annotated tag |
|
| `message` | string | no | Creates annotated tag |
|
||||||
|
@ -172,5 +174,5 @@ Parameters:
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| ---------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
| ---------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||||
| `tag_name` | string | yes | The name of a tag |
|
| `tag_name` | string | yes | The name of a tag |
|
||||||
|
|
|
@ -168,7 +168,7 @@ Required options:
|
||||||
```yaml
|
```yaml
|
||||||
time_frame: all
|
time_frame: all
|
||||||
data_source: redis
|
data_source: redis
|
||||||
instrumentation_class: 'RedisMetric'
|
instrumentation_class: RedisMetric
|
||||||
options:
|
options:
|
||||||
event: pushes
|
event: pushes
|
||||||
prefix: source_code
|
prefix: source_code
|
||||||
|
@ -199,7 +199,7 @@ You must also use the class's name in the YAML setup.
|
||||||
```yaml
|
```yaml
|
||||||
time_frame: all
|
time_frame: all
|
||||||
data_source: redis
|
data_source: redis
|
||||||
instrumentation_class: 'MergeUsageCountRedisMetric'
|
instrumentation_class: MergeUsageCountRedisMetric
|
||||||
options:
|
options:
|
||||||
event: pushes
|
event: pushes
|
||||||
prefix: source_code
|
prefix: source_code
|
||||||
|
@ -217,7 +217,7 @@ Count unique values for `i_quickactions_approve` event.
|
||||||
```yaml
|
```yaml
|
||||||
time_frame: 28d
|
time_frame: 28d
|
||||||
data_source: redis_hll
|
data_source: redis_hll
|
||||||
instrumentation_class: 'RedisHLLMetric'
|
instrumentation_class: RedisHLLMetric
|
||||||
options:
|
options:
|
||||||
events:
|
events:
|
||||||
- i_quickactions_approve
|
- i_quickactions_approve
|
||||||
|
@ -248,7 +248,7 @@ You must also use the class's name in the YAML setup.
|
||||||
```yaml
|
```yaml
|
||||||
time_frame: 28d
|
time_frame: 28d
|
||||||
data_source: redis_hll
|
data_source: redis_hll
|
||||||
instrumentation_class: 'MergeUsageCountRedisHLLMetric'
|
instrumentation_class: MergeUsageCountRedisHLLMetric
|
||||||
options:
|
options:
|
||||||
events:
|
events:
|
||||||
- i_quickactions_approve
|
- i_quickactions_approve
|
||||||
|
@ -288,7 +288,7 @@ You must also include the instrumentation class name in the YAML setup.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
time_frame: 28d
|
time_frame: 28d
|
||||||
instrumentation_class: 'IssuesBoardsCountMetric'
|
instrumentation_class: IssuesBoardsCountMetric
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generic metrics
|
## Generic metrics
|
||||||
|
@ -345,7 +345,7 @@ The generator takes the class name as an argument and the following options:
|
||||||
- `--ee` Indicates if the metric is for EE.
|
- `--ee` Indicates if the metric is for EE.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
rails generate gitlab:usage_metric CountIssues --type database
|
rails generate gitlab:usage_metric CountIssues --type database --operation distinct_count
|
||||||
create lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
|
create lib/gitlab/usage/metrics/instrumentations/count_issues_metric.rb
|
||||||
create spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
|
create spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
|
||||||
```
|
```
|
||||||
|
|
|
@ -16,7 +16,6 @@ GitLab has two types of namespaces:
|
||||||
read about [repository redirects](../project/repository/index.md#what-happens-when-a-repository-path-changes).
|
read about [repository redirects](../project/repository/index.md#what-happens-when-a-repository-path-changes).
|
||||||
- You cannot create subgroups in a personal namespace.
|
- You cannot create subgroups in a personal namespace.
|
||||||
- Groups in your namespace do not inherit your namespace permissions and group features.
|
- Groups in your namespace do not inherit your namespace permissions and group features.
|
||||||
- Creating subgroups under this namespace is not allowed.
|
|
||||||
- All the *Personal Projects* created will fall under the scope of this namespace.
|
- All the *Personal Projects* created will fall under the scope of this namespace.
|
||||||
|
|
||||||
- A *group* or *subgroup* namespace:
|
- A *group* or *subgroup* namespace:
|
||||||
|
|
|
@ -102,6 +102,10 @@ To delete a task:
|
||||||
|
|
||||||
To show who is responsible for a task, you can assign users to it.
|
To show who is responsible for a task, you can assign users to it.
|
||||||
|
|
||||||
|
Users on GitLab Free can assign one user per task.
|
||||||
|
Users on GitLab Premium and higher can assign multiple users to a single task.
|
||||||
|
See also [multiple assignees for issues](project/issues/multiple_assignees_for_issues.md).
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
- You must have at least the Reporter role for the project.
|
- You must have at least the Reporter role for the project.
|
||||||
|
|
|
@ -248,6 +248,8 @@ module API
|
||||||
|
|
||||||
authorize! :admin_group, group
|
authorize! :admin_group, group
|
||||||
|
|
||||||
|
group.remove_avatar! if params.key?(:avatar) && params[:avatar].nil?
|
||||||
|
|
||||||
if update_group(group)
|
if update_group(group)
|
||||||
present_group_details(params, group, with_projects: true)
|
present_group_details(params, group, with_projects: true)
|
||||||
else
|
else
|
||||||
|
|
|
@ -100,6 +100,62 @@ module API
|
||||||
present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description]
|
present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Download a project release asset file' do
|
||||||
|
detail 'This feature was introduced in GitLab 15.4.'
|
||||||
|
named 'download_release_asset_file'
|
||||||
|
end
|
||||||
|
params do
|
||||||
|
requires :tag_name, type: String,
|
||||||
|
desc: 'The name of the tag.', as: :tag
|
||||||
|
requires :file_path, type: String,
|
||||||
|
file_path: true,
|
||||||
|
desc: 'The path to the file to download, as specified when creating the release asset.'
|
||||||
|
end
|
||||||
|
route_setting :authentication, job_token_allowed: true
|
||||||
|
get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||||
|
authorize_download_code!
|
||||||
|
|
||||||
|
not_found! unless release
|
||||||
|
|
||||||
|
link = release.links.find_by_filepath!("/#{params[:file_path]}")
|
||||||
|
|
||||||
|
not_found! unless link
|
||||||
|
|
||||||
|
redirect link.url
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Get the latest project release' do
|
||||||
|
detail 'This feature was introduced in GitLab 15.4.'
|
||||||
|
named 'get_latest_release'
|
||||||
|
end
|
||||||
|
params do
|
||||||
|
requires :suffix_path, type: String, file_path: true, desc: 'The path to be suffixed to the latest release'
|
||||||
|
end
|
||||||
|
route_setting :authentication, job_token_allowed: true
|
||||||
|
get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
|
||||||
|
authorize_download_code!
|
||||||
|
|
||||||
|
# Try to find the latest release
|
||||||
|
latest_release = find_latest_release
|
||||||
|
not_found! unless latest_release
|
||||||
|
|
||||||
|
# Build the full API URL with the tag of the latest release
|
||||||
|
redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag)
|
||||||
|
|
||||||
|
# Include the additional suffix_path if present
|
||||||
|
redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present?
|
||||||
|
|
||||||
|
# Include any query parameter except `order_by` since we have plans to extend it in the future.
|
||||||
|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference.
|
||||||
|
query_parameters_except_order_by = get_query_params.except('order_by')
|
||||||
|
|
||||||
|
if query_parameters_except_order_by.present?
|
||||||
|
redirect_url += "?#{query_parameters_except_order_by.compact.to_param}"
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect redirect_url
|
||||||
|
end
|
||||||
|
|
||||||
desc 'Create a new release' do
|
desc 'Create a new release' do
|
||||||
detail 'This feature was introduced in GitLab 11.7.'
|
detail 'This feature was introduced in GitLab 11.7.'
|
||||||
named 'create_release'
|
named 'create_release'
|
||||||
|
@ -232,6 +288,16 @@ module API
|
||||||
@release ||= user_project.releases.find_by_tag(params[:tag])
|
@release ||= user_project.releases.find_by_tag(params[:tag])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_latest_release
|
||||||
|
ReleasesFinder.new(user_project, current_user, { order_by: 'released_at', sort: 'desc' }).execute.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_query_params
|
||||||
|
return {} unless @request.query_string.present?
|
||||||
|
|
||||||
|
Rack::Utils.parse_nested_query(@request.query_string)
|
||||||
|
end
|
||||||
|
|
||||||
def log_release_created_audit_event(release)
|
def log_release_created_audit_event(release)
|
||||||
# extended in EE
|
# extended in EE
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::<%= class_name %>Metric do
|
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::<%= class_name %>Metric do
|
||||||
it_behaves_like 'a correct instrumented metric value', {}, 1
|
let(:expected_value) { 1 }
|
||||||
|
|
||||||
|
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,9 @@ module Gitlab
|
||||||
def ensure_environment(build)
|
def ensure_environment(build)
|
||||||
return unless build.instance_of?(::Ci::Build) && build.has_environment?
|
return unless build.instance_of?(::Ci::Build) && build.has_environment?
|
||||||
|
|
||||||
environment = ::Gitlab::Ci::Pipeline::Seed::Environment.new(build).to_resource
|
environment = ::Gitlab::Ci::Pipeline::Seed::Environment
|
||||||
|
.new(build, merge_request: @command.merge_request)
|
||||||
|
.to_resource
|
||||||
|
|
||||||
if environment.persisted?
|
if environment.persisted?
|
||||||
build.persisted_environment = environment
|
build.persisted_environment = environment
|
||||||
|
|
|
@ -5,12 +5,13 @@ module Gitlab
|
||||||
module Pipeline
|
module Pipeline
|
||||||
module Seed
|
module Seed
|
||||||
class Environment < Seed::Base
|
class Environment < Seed::Base
|
||||||
attr_reader :job
|
attr_reader :job, :merge_request
|
||||||
|
|
||||||
delegate :simple_variables, to: :job
|
delegate :simple_variables, to: :job
|
||||||
|
|
||||||
def initialize(job)
|
def initialize(job, merge_request: nil)
|
||||||
@job = job
|
@job = job
|
||||||
|
@merge_request = merge_request
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_resource
|
def to_resource
|
||||||
|
@ -18,6 +19,7 @@ module Gitlab
|
||||||
# Initialize the attributes at creation
|
# Initialize the attributes at creation
|
||||||
environment.auto_stop_in = expanded_auto_stop_in
|
environment.auto_stop_in = expanded_auto_stop_in
|
||||||
environment.tier = deployment_tier
|
environment.tier = deployment_tier
|
||||||
|
environment.merge_request = merge_request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -30403,6 +30403,9 @@ msgstr ""
|
||||||
msgid "Profiles|Connected Accounts"
|
msgid "Profiles|Connected Accounts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Created%{time_ago}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Current path: %{path}"
|
msgid "Profiles|Current path: %{path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,63 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "spec_helper"
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe "User creates branch", :js do
|
RSpec.describe 'User creates branch', :js do
|
||||||
include Spec::Support::Helpers::Features::BranchesHelpers
|
include Spec::Support::Helpers::Features::BranchesHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
let(:project) { create(:project, :repository) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
shared_examples 'creates new branch' do
|
||||||
|
specify do
|
||||||
|
branch_name = "deploy_keys_#{SecureRandom.hex(4)}"
|
||||||
|
|
||||||
|
create_branch(branch_name)
|
||||||
|
|
||||||
|
expect(page).to have_content(branch_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'renders not found page' do
|
||||||
|
specify do
|
||||||
|
expect(page).to have_title('Not Found')
|
||||||
|
expect(page).to have_content('Page Not Found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public with private repository' do
|
||||||
|
let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
|
||||||
|
|
||||||
|
context 'when user is an inherited member from the group' do
|
||||||
|
context 'and user is a guest' do
|
||||||
|
before do
|
||||||
|
group.add_guest(user)
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
visit(new_project_branch_path(project))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'renders not found page'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
visit(new_project_branch_path(project))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates new branch'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
let_it_be(:project) { create(:project, :private, :repository, group: group) }
|
||||||
|
|
||||||
|
context 'when user is a direct project member' do
|
||||||
|
context 'and user is a developer' do
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
@ -15,7 +65,7 @@ RSpec.describe "User creates branch", :js do
|
||||||
visit(new_project_branch_path(project))
|
visit(new_project_branch_path(project))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on new branch page' do
|
context 'when on new branch page' do
|
||||||
it 'renders I18n supported text' do
|
it 'renders I18n supported text' do
|
||||||
page.within('#new-branch-form') do
|
page.within('#new-branch-form') do
|
||||||
expect(page).to have_content(_('Branch name'))
|
expect(page).to have_content(_('Branch name'))
|
||||||
|
@ -25,34 +75,55 @@ RSpec.describe "User creates branch", :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates new branch" do
|
it_behaves_like 'creates new branch'
|
||||||
branch_name = "deploy_keys"
|
|
||||||
|
|
||||||
create_branch(branch_name)
|
context 'when branch name is invalid' do
|
||||||
|
it 'does not create new branch' do
|
||||||
|
invalid_branch_name = '1.0 stable'
|
||||||
|
|
||||||
expect(page).to have_content(branch_name)
|
fill_in('branch_name', with: invalid_branch_name)
|
||||||
end
|
page.find('body').click # defocus the branch_name input
|
||||||
|
|
||||||
context "when branch name is invalid" do
|
select_branch('master')
|
||||||
it "does not create new branch" do
|
click_button('Create branch')
|
||||||
invalid_branch_name = "1.0 stable"
|
|
||||||
|
|
||||||
fill_in("branch_name", with: invalid_branch_name)
|
expect(page).to have_content('Branch name is invalid')
|
||||||
page.find("body").click # defocus the branch_name input
|
|
||||||
|
|
||||||
select_branch("master")
|
|
||||||
click_button("Create branch")
|
|
||||||
|
|
||||||
expect(page).to have_content("Branch name is invalid")
|
|
||||||
expect(page).to have_content("can't contain spaces")
|
expect(page).to have_content("can't contain spaces")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when branch name already exists" do
|
context 'when branch name already exists' do
|
||||||
it "does not create new branch" do
|
it 'does not create new branch' do
|
||||||
create_branch("master")
|
create_branch('master')
|
||||||
|
|
||||||
expect(page).to have_content("Branch already exists")
|
expect(page).to have_content('Branch already exists')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is an inherited member from the group' do
|
||||||
|
context 'and user is a guest' do
|
||||||
|
before do
|
||||||
|
group.add_guest(user)
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
visit(new_project_branch_path(project))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'renders not found page'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
visit(new_project_branch_path(project))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates new branch'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'Runners' do
|
RSpec.describe 'Runners' do
|
||||||
let(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
@ -24,25 +24,25 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a project has enabled shared_runners' do
|
context 'when a project has enabled shared_runners' do
|
||||||
let(:project) { create(:project) }
|
let_it_be(:project) { create(:project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a project_type runner is activated on the project' do
|
context 'when a project_type runner is activated on the project' do
|
||||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
|
||||||
|
|
||||||
it 'user sees the specific runner' do
|
it 'user sees the specific runner' do
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
|
|
||||||
within '.activated-specific-runners' do
|
within '.activated-specific-runners' do
|
||||||
expect(page).to have_content(specific_runner.display_name)
|
expect(page).to have_content(project_runner.display_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
click_on specific_runner.short_sha
|
click_on project_runner.short_sha
|
||||||
|
|
||||||
expect(page).to have_content(specific_runner.platform)
|
expect(page).to have_content(project_runner.platform)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'user can pause and resume the specific runner' do
|
it 'user can pause and resume the specific runner' do
|
||||||
|
@ -72,7 +72,7 @@ RSpec.describe 'Runners' do
|
||||||
click_on 'Remove runner'
|
click_on 'Remove runner'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).not_to have_content(specific_runner.display_name)
|
expect(page).not_to have_content(project_runner.display_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'user edits the runner to be protected' do
|
it 'user edits the runner to be protected' do
|
||||||
|
@ -92,7 +92,7 @@ RSpec.describe 'Runners' do
|
||||||
|
|
||||||
context 'when a runner has a tag' do
|
context 'when a runner has a tag' do
|
||||||
before do
|
before do
|
||||||
specific_runner.update!(tag_list: ['tag'])
|
project_runner.update!(tag_list: ['tag'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'user edits runner not to run untagged jobs' do
|
it 'user edits runner not to run untagged jobs' do
|
||||||
|
@ -120,11 +120,9 @@ RSpec.describe 'Runners' do
|
||||||
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
|
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when multiple runners are configured' do
|
context 'when multiple runners are configured' do
|
||||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
let!(:project_runner_2) { create(:ci_runner, :project, projects: [project]) }
|
||||||
let!(:specific_runner_2) { create(:ci_runner, :project, projects: [project]) }
|
|
||||||
|
|
||||||
it 'adds pagination to the runner list' do
|
it 'adds pagination to the runner list' do
|
||||||
stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
|
stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
|
||||||
|
@ -134,10 +132,11 @@ RSpec.describe 'Runners' do
|
||||||
expect(find('.pagination')).not_to be_nil
|
expect(find('.pagination')).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when a specific runner exists in another project' do
|
context 'when a specific runner exists in another project' do
|
||||||
let(:another_project) { create(:project) }
|
let(:another_project) { create(:project) }
|
||||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
|
let!(:project_runner) { create(:ci_runner, :project, projects: [another_project]) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
another_project.add_maintainer(user)
|
another_project.add_maintainer(user)
|
||||||
|
@ -150,13 +149,13 @@ RSpec.describe 'Runners' do
|
||||||
click_on 'Enable for this project'
|
click_on 'Enable for this project'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page.find('.activated-specific-runners')).to have_content(specific_runner.display_name)
|
expect(page.find('.activated-specific-runners')).to have_content(project_runner.display_name)
|
||||||
|
|
||||||
within '.activated-specific-runners' do
|
within '.activated-specific-runners' do
|
||||||
click_on 'Disable for this project'
|
click_on 'Disable for this project'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page.find('.available-specific-runners')).to have_content(specific_runner.display_name)
|
expect(page.find('.available-specific-runners')).to have_content(project_runner.display_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -255,7 +254,8 @@ RSpec.describe 'Runners' do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:group) { create :group }
|
let_it_be(:group) { create :group }
|
||||||
|
let_it_be(:project) { create :project, group: group }
|
||||||
|
|
||||||
context 'as project and group maintainer' do
|
context 'as project and group maintainer' do
|
||||||
before do
|
before do
|
||||||
|
@ -263,8 +263,6 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'project with a group but no group runner' do
|
context 'project with a group but no group runner' do
|
||||||
let(:project) { create :project, group: group }
|
|
||||||
|
|
||||||
it 'group runners are not available' do
|
it 'group runners are not available' do
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
|
|
||||||
|
@ -280,8 +278,6 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'project with a group but no group runner' do
|
context 'project with a group but no group runner' do
|
||||||
let(:project) { create :project, group: group }
|
|
||||||
|
|
||||||
it 'group runners are available' do
|
it 'group runners are available' do
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
|
|
||||||
|
@ -304,10 +300,11 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'project with a group but no group runner' do
|
context 'with group project' do
|
||||||
let(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let(:project) { create(:project, group: group) }
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
|
||||||
|
context 'project with a group but no group runner' do
|
||||||
it 'group runners are not available' do
|
it 'group runners are not available' do
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
|
|
||||||
|
@ -319,9 +316,9 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'project with a group and a group runner' do
|
context 'project with a group and a group runner' do
|
||||||
let(:group) { create(:group) }
|
let_it_be(:ci_runner) do
|
||||||
let(:project) { create(:project, group: group) }
|
create(:ci_runner, :group, groups: [group], description: 'group-runner')
|
||||||
let!(:ci_runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
|
end
|
||||||
|
|
||||||
it 'group runners are available' do
|
it 'group runners are available' do
|
||||||
visit project_runners_path(project)
|
visit project_runners_path(project)
|
||||||
|
@ -346,4 +343,5 @@ RSpec.describe 'Runners' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric do
|
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric do
|
||||||
it_behaves_like 'a correct instrumented metric value', {}, 1
|
let(:expected_value) { 1 }
|
||||||
|
|
||||||
|
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
|
RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failures do
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:stage) { build(:ci_stage, project: project, statuses: [job]) }
|
let(:stage) { build(:ci_stage, project: project, statuses: [job]) }
|
||||||
let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) }
|
let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) }
|
||||||
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||||
|
let(:environment) { project.environments.find_by_name('review/master') }
|
||||||
|
|
||||||
let(:command) do
|
let(:command) do
|
||||||
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
|
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
|
||||||
|
@ -24,14 +26,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
|
||||||
context 'when a pipeline contains a deployment job' do
|
context 'when a pipeline contains a deployment job' do
|
||||||
let!(:job) { build(:ci_build, :start_review_app, project: project) }
|
let!(:job) { build(:ci_build, :start_review_app, project: project) }
|
||||||
|
|
||||||
it 'ensures environment existence for the job' do
|
context 'and the environment does not exist' do
|
||||||
|
it 'creates the environment specified by the job' do
|
||||||
expect { subject }.to change { Environment.count }.by(1)
|
expect { subject }.to change { Environment.count }.by(1)
|
||||||
|
|
||||||
expect(project.environments.find_by_name('review/master')).to be_present
|
expect(environment).to be_present
|
||||||
expect(job.persisted_environment.name).to eq('review/master')
|
expect(job.persisted_environment.name).to eq('review/master')
|
||||||
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'and the pipeline is for a merge request' do
|
||||||
|
let(:command) do
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'associates the environment with the merge request' do
|
||||||
|
expect { subject }.to change { Environment.count }.by(1)
|
||||||
|
|
||||||
|
expect(environment.merge_request).to eq(merge_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when an environment has already been existed' do
|
context 'when an environment has already been existed' do
|
||||||
before do
|
before do
|
||||||
create(:environment, project: project, name: 'review/master')
|
create(:environment, project: project, name: 'review/master')
|
||||||
|
@ -40,10 +56,22 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
|
||||||
it 'ensures environment existence for the job' do
|
it 'ensures environment existence for the job' do
|
||||||
expect { subject }.not_to change { Environment.count }
|
expect { subject }.not_to change { Environment.count }
|
||||||
|
|
||||||
expect(project.environments.find_by_name('review/master')).to be_present
|
expect(environment).to be_present
|
||||||
expect(job.persisted_environment.name).to eq('review/master')
|
expect(job.persisted_environment.name).to eq('review/master')
|
||||||
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'and the pipeline is for a merge request' do
|
||||||
|
let(:command) do
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not associate the environment with the merge request' do
|
||||||
|
expect { subject }.not_to change { Environment.count }
|
||||||
|
|
||||||
|
expect(environment.merge_request).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when an environment name contains an invalid character' do
|
context 'when an environment name contains an invalid character' do
|
||||||
|
@ -65,7 +93,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do
|
||||||
it 'ensures environment existence for the job' do
|
it 'ensures environment existence for the job' do
|
||||||
expect { subject }.to change { Environment.count }.by(1)
|
expect { subject }.to change { Environment.count }.by(1)
|
||||||
|
|
||||||
expect(project.environments.find_by_name('review/master')).to be_present
|
expect(environment).to be_present
|
||||||
expect(job.persisted_environment.name).to eq('review/master')
|
expect(job.persisted_environment.name).to eq('review/master')
|
||||||
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
expect(job.metadata.expanded_environment_name).to eq('review/master')
|
||||||
end
|
end
|
||||||
|
|
|
@ -191,5 +191,34 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
|
||||||
|
|
||||||
it_behaves_like 'returning a correct environment'
|
it_behaves_like 'returning a correct environment'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when merge_request is provided' do
|
||||||
|
let(:environment_name) { 'development' }
|
||||||
|
let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } }
|
||||||
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||||
|
let(:seed) { described_class.new(job, merge_request: merge_request) }
|
||||||
|
|
||||||
|
context 'and environment does not exist' do
|
||||||
|
let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
|
||||||
|
|
||||||
|
it 'creates an environment associated with the merge request' do
|
||||||
|
expect { subject }.to change { Environment.count }.by(1)
|
||||||
|
|
||||||
|
expect(subject.merge_request).to eq(merge_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and environment already exists' do
|
||||||
|
before do
|
||||||
|
create(:environment, project: project, name: environment_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not change the merge request associated with the environment' do
|
||||||
|
expect { subject }.not_to change { Environment.count }
|
||||||
|
|
||||||
|
expect(subject.merge_request).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -211,6 +211,7 @@ merge_requests:
|
||||||
- user_note_authors
|
- user_note_authors
|
||||||
- cleanup_schedule
|
- cleanup_schedule
|
||||||
- compliance_violations
|
- compliance_violations
|
||||||
|
- created_environments
|
||||||
external_pull_requests:
|
external_pull_requests:
|
||||||
- project
|
- project
|
||||||
merge_request_diff:
|
merge_request_diff:
|
||||||
|
|
|
@ -17,6 +17,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
||||||
it { is_expected.to nullify_if_blank(:external_url) }
|
it { is_expected.to nullify_if_blank(:external_url) }
|
||||||
|
|
||||||
it { is_expected.to belong_to(:project).required }
|
it { is_expected.to belong_to(:project).required }
|
||||||
|
it { is_expected.to belong_to(:merge_request).optional }
|
||||||
|
|
||||||
it { is_expected.to have_many(:deployments) }
|
it { is_expected.to have_many(:deployments) }
|
||||||
it { is_expected.to have_many(:metrics_dashboard_annotations) }
|
it { is_expected.to have_many(:metrics_dashboard_annotations) }
|
||||||
it { is_expected.to have_many(:alert_management_alerts) }
|
it { is_expected.to have_many(:alert_management_alerts) }
|
||||||
|
@ -40,6 +42,26 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
||||||
|
|
||||||
expect(environment).to be_valid
|
expect(environment).to be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'does not allow changes to merge_request' do
|
||||||
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||||
|
|
||||||
|
it 'for an environment that has no merge request associated' do
|
||||||
|
environment = create(:environment)
|
||||||
|
|
||||||
|
environment.merge_request = merge_request
|
||||||
|
|
||||||
|
expect(environment).not_to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'for an environment that has a merge request associated' do
|
||||||
|
environment = create(:environment, merge_request: merge_request)
|
||||||
|
|
||||||
|
environment.merge_request = nil
|
||||||
|
|
||||||
|
expect(environment).not_to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validate and sanitize external url' do
|
describe 'validate and sanitize external url' do
|
||||||
|
|
|
@ -31,6 +31,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
||||||
it { is_expected.to have_many(:draft_notes) }
|
it { is_expected.to have_many(:draft_notes) }
|
||||||
it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
|
it { is_expected.to have_many(:reviews).inverse_of(:merge_request) }
|
||||||
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
|
it { is_expected.to have_one(:cleanup_schedule).inverse_of(:merge_request) }
|
||||||
|
it { is_expected.to have_many(:created_environments).class_name('Environment').inverse_of(:merge_request) }
|
||||||
|
|
||||||
context 'for forks' do
|
context 'for forks' do
|
||||||
let!(:project) { create(:project) }
|
let!(:project) { create(:project) }
|
||||||
|
|
|
@ -5,13 +5,21 @@ require 'spec_helper'
|
||||||
RSpec.describe API::Files do
|
RSpec.describe API::Files do
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
|
let_it_be_with_refind(:user) { create(:user) }
|
||||||
|
let_it_be(:inherited_guest) { create(:user) }
|
||||||
|
let_it_be(:inherited_reporter) { create(:user) }
|
||||||
|
let_it_be(:inherited_developer) { create(:user) }
|
||||||
|
|
||||||
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
|
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
|
||||||
let(:guest) { create(:user) { |u| project.add_guest(u) } }
|
let(:guest) { create(:user) { |u| project.add_guest(u) } }
|
||||||
let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
|
let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' }
|
||||||
let(:executable_file_path) { "files%2Fexecutables%2Fls" }
|
let(:file_name) { 'popen.rb' }
|
||||||
let(:rouge_file_path) { "%2e%2e%2f" }
|
let(:last_commit_id) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
|
||||||
let(:absolute_path) { "%2Fetc%2Fpasswd.rb" }
|
let(:content_sha256) { 'c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887' }
|
||||||
|
let(:executable_file_path) { 'files%2Fexecutables%2Fls' }
|
||||||
|
let(:invalid_file_path) { '%2e%2e%2f' }
|
||||||
|
let(:absolute_path) { '%2Fetc%2Fpasswd.rb' }
|
||||||
let(:invalid_file_message) { 'file_path should be a valid file path' }
|
let(:invalid_file_message) { 'file_path should be a valid file path' }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
|
@ -46,6 +54,12 @@ RSpec.describe API::Files do
|
||||||
fake_class.new
|
fake_class.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
group.add_guest(inherited_guest)
|
||||||
|
group.add_reporter(inherited_reporter)
|
||||||
|
group.add_developer(inherited_developer)
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
@ -70,10 +84,12 @@ RSpec.describe API::Files do
|
||||||
expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' })
|
expect(helper.headers).to eq({ 'X-Gitlab-Test' => '1' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises exception if value is an Enumerable' do
|
context 'when value is an Enumerable' do
|
||||||
|
it 'raises an exception' do
|
||||||
expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
|
expect { helper.set_http_headers(test: [1]) }.to raise_error(ArgumentError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
shared_examples 'when path is absolute' do
|
shared_examples 'when path is absolute' do
|
||||||
it 'returns 400 when file path is absolute' do
|
it 'returns 400 when file path is absolute' do
|
||||||
|
@ -87,12 +103,12 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "HEAD /projects/:id/repository/files/:file_path" do
|
describe 'HEAD /projects/:id/repository/files/:file_path' do
|
||||||
shared_examples_for 'repository files' do
|
shared_examples_for 'repository files' do
|
||||||
let(:options) { {} }
|
let(:options) { {} }
|
||||||
|
|
||||||
it 'returns 400 when file path is invalid' do
|
it 'returns 400 when file path is invalid' do
|
||||||
head api(route(rouge_file_path), current_user, **options), params: params
|
head api(route(invalid_file_path), current_user, **options), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
@ -106,16 +122,16 @@ RSpec.describe API::Files do
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
|
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
|
||||||
expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
|
expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
|
||||||
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
|
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
|
||||||
expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
|
expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
|
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
|
||||||
head api(route(file_path), current_user, **options), params: params
|
head api(route(file_path), current_user, **options), params: params
|
||||||
|
|
||||||
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
|
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
|
||||||
.to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
|
.to eq(content_sha256)
|
||||||
|
|
||||||
expect_next_instance_of(Gitlab::Git::Blob) do |instance|
|
expect_next_instance_of(Gitlab::Git::Blob) do |instance|
|
||||||
expect(instance).not_to receive(:load_all_data!)
|
expect(instance).not_to receive(:load_all_data!)
|
||||||
|
@ -126,8 +142,8 @@ RSpec.describe API::Files do
|
||||||
|
|
||||||
it 'returns file by commit sha' do
|
it 'returns file by commit sha' do
|
||||||
# This file is deleted on HEAD
|
# This file is deleted on HEAD
|
||||||
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
|
file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
|
||||||
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
|
params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
|
||||||
|
|
||||||
head api(route(file_path), current_user, **options), params: params
|
head api(route(file_path), current_user, **options), params: params
|
||||||
|
|
||||||
|
@ -137,15 +153,15 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when mandatory params are not given' do
|
context 'when mandatory params are not given' do
|
||||||
it "responds with a 400 status" do
|
it 'responds with a 400 status' do
|
||||||
head api(route("any%2Ffile"), current_user, **options)
|
head api(route('any%2Ffile'), current_user, **options)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when file_path does not exist' do
|
context 'when file_path does not exist' do
|
||||||
it "responds with a 404 status" do
|
it 'responds with a 404 status' do
|
||||||
params[:ref] = 'master'
|
params[:ref] = 'master'
|
||||||
|
|
||||||
head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params
|
head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params
|
||||||
|
@ -157,7 +173,7 @@ RSpec.describe API::Files do
|
||||||
context 'when file_path does not exist' do
|
context 'when file_path does not exist' do
|
||||||
include_context 'disabled repository'
|
include_context 'disabled repository'
|
||||||
|
|
||||||
it "responds with a 403 status" do
|
it 'responds with a 403 status' do
|
||||||
head api(route(file_path), current_user, **options), params: params
|
head api(route(file_path), current_user, **options), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
@ -165,15 +181,16 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is public' do
|
context 'when unauthenticated' do
|
||||||
|
context 'and project is public' do
|
||||||
it_behaves_like 'repository files' do
|
it_behaves_like 'repository files' do
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:current_user) { nil }
|
let(:current_user) { nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is private' do
|
context 'and project is private' do
|
||||||
it "responds with a 404 status" do
|
it 'responds with a 404 status' do
|
||||||
current_user = nil
|
current_user = nil
|
||||||
|
|
||||||
head api(route(file_path), current_user), params: params
|
head api(route(file_path), current_user), params: params
|
||||||
|
@ -181,6 +198,7 @@ RSpec.describe API::Files do
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when PATs are used' do
|
context 'when PATs are used' do
|
||||||
it_behaves_like 'repository files' do
|
it_behaves_like 'repository files' do
|
||||||
|
@ -190,25 +208,41 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a developer' do
|
context 'when authenticated' do
|
||||||
|
context 'and user is a developer' do
|
||||||
it_behaves_like 'repository files' do
|
it_behaves_like 'repository files' do
|
||||||
let(:current_user) { user }
|
let(:current_user) { user }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a guest' do
|
context 'and user is a guest' do
|
||||||
it_behaves_like '403 response' do
|
it_behaves_like '403 response' do
|
||||||
let(:request) { head api(route(file_path), guest), params: params }
|
let(:request) { head api(route(file_path), guest), params: params }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "GET /projects/:id/repository/files/:file_path" do
|
describe 'GET /projects/:id/repository/files/:file_path' do
|
||||||
shared_examples_for 'repository files' do
|
|
||||||
let(:options) { {} }
|
let(:options) { {} }
|
||||||
|
|
||||||
|
shared_examples 'returns non-executable file attributes as json' do
|
||||||
|
specify do
|
||||||
|
get api(route(file_path), api_user, **options), params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
|
||||||
|
expect(json_response['file_name']).to eq(file_name)
|
||||||
|
expect(json_response['last_commit_id']).to eq(last_commit_id)
|
||||||
|
expect(json_response['content_sha256']).to eq(content_sha256)
|
||||||
|
expect(json_response['execute_filemode']).to eq(false)
|
||||||
|
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'repository files' do
|
||||||
it 'returns 400 for invalid file path' do
|
it 'returns 400 for invalid file path' do
|
||||||
get api(route(rouge_file_path), api_user, **options), params: params
|
get api(route(invalid_file_path), api_user, **options), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
|
@ -218,17 +252,7 @@ RSpec.describe API::Files do
|
||||||
subject { get api(route(absolute_path), api_user, **options), params: params }
|
subject { get api(route(absolute_path), api_user, **options), params: params }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns file attributes as json' do
|
it_behaves_like 'returns non-executable file attributes as json'
|
||||||
get api(route(file_path), api_user, **options), params: params
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
|
|
||||||
expect(json_response['file_name']).to eq('popen.rb')
|
|
||||||
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
|
|
||||||
expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
|
|
||||||
expect(json_response['execute_filemode']).to eq(false)
|
|
||||||
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for executable file' do
|
context 'for executable file' do
|
||||||
it 'returns file attributes as json' do
|
it 'returns file attributes as json' do
|
||||||
|
@ -247,7 +271,7 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns json when file has txt extension' do
|
it 'returns json when file has txt extension' do
|
||||||
file_path = "bar%2Fbranch-test.txt"
|
file_path = 'bar%2Fbranch-test.txt'
|
||||||
|
|
||||||
get api(route(file_path), api_user, **options), params: params
|
get api(route(file_path), api_user, **options), params: params
|
||||||
|
|
||||||
|
@ -277,8 +301,8 @@ RSpec.describe API::Files do
|
||||||
|
|
||||||
it 'returns file by commit sha' do
|
it 'returns file by commit sha' do
|
||||||
# This file is deleted on HEAD
|
# This file is deleted on HEAD
|
||||||
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
|
file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
|
||||||
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
|
params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
|
||||||
|
|
||||||
get api(route(file_path), api_user, **options), params: params
|
get api(route(file_path), api_user, **options), params: params
|
||||||
|
|
||||||
|
@ -289,9 +313,9 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns raw file info' do
|
it 'returns raw file info' do
|
||||||
url = route(file_path) + "/raw"
|
url = route(file_path) + '/raw'
|
||||||
expect_to_send_git_blob(api(url, api_user, **options), params)
|
expect_to_send_git_blob(api(url, api_user, **options), params)
|
||||||
expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
|
expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns blame file info' do
|
it 'returns blame file info' do
|
||||||
|
@ -303,16 +327,16 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets inline content disposition by default' do
|
it 'sets inline content disposition by default' do
|
||||||
url = route(file_path) + "/raw"
|
url = route(file_path) + '/raw'
|
||||||
|
|
||||||
get api(url, api_user, **options), params: params
|
get api(url, api_user, **options), params: params
|
||||||
|
|
||||||
expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb))
|
expect(headers['Content-Disposition']).to eq(%(inline; filename="#{file_name}"; filename*=UTF-8''#{file_name}))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when mandatory params are not given' do
|
context 'when mandatory params are not given' do
|
||||||
it_behaves_like '400 response' do
|
it_behaves_like '400 response' do
|
||||||
let(:request) { get api(route("any%2Ffile"), current_user, **options) }
|
let(:request) { get api(route('any%2Ffile'), current_user, **options) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -334,7 +358,8 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is public' do
|
context 'when unauthenticated' do
|
||||||
|
context 'and project is public' do
|
||||||
it_behaves_like 'repository files' do
|
it_behaves_like 'repository files' do
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:current_user) { nil }
|
let(:current_user) { nil }
|
||||||
|
@ -342,7 +367,24 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when PATs are used' do
|
context 'and project is private' do
|
||||||
|
it_behaves_like '404 response' do
|
||||||
|
let(:request) { get api(route(file_path)), params: params }
|
||||||
|
let(:message) { '404 Project Not Found' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authenticated' do
|
||||||
|
context 'and user is a direct project member' do
|
||||||
|
context 'and project is private' do
|
||||||
|
context 'and user is a developer' do
|
||||||
|
it_behaves_like 'repository files' do
|
||||||
|
let(:current_user) { user }
|
||||||
|
let(:api_user) { user }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and PATs are used' do
|
||||||
it_behaves_like 'repository files' do
|
it_behaves_like 'repository files' do
|
||||||
let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
|
let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
|
||||||
let(:current_user) { user }
|
let(:current_user) { user }
|
||||||
|
@ -350,27 +392,65 @@ RSpec.describe API::Files do
|
||||||
let(:options) { { personal_access_token: token } }
|
let(:options) { { personal_access_token: token } }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is private' do
|
|
||||||
it_behaves_like '404 response' do
|
|
||||||
let(:request) { get api(route(file_path)), params: params }
|
|
||||||
let(:message) { '404 Project Not Found' }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a developer' do
|
context 'and user is a guest' do
|
||||||
it_behaves_like 'repository files' do
|
|
||||||
let(:current_user) { user }
|
|
||||||
let(:api_user) { user }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when authenticated', 'as a guest' do
|
|
||||||
it_behaves_like '403 response' do
|
it_behaves_like '403 response' do
|
||||||
let(:request) { get api(route(file_path), guest), params: params }
|
let(:request) { get api(route(file_path), guest), params: params }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authenticated' do
|
||||||
|
context 'and user is an inherited member from the group' do
|
||||||
|
context 'when project is public with private repository' do
|
||||||
|
let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
|
||||||
|
|
||||||
|
context 'and user is a guest' do
|
||||||
|
it_behaves_like 'returns non-executable file attributes as json' do
|
||||||
|
let(:api_user) { inherited_guest }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a reporter' do
|
||||||
|
it_behaves_like 'returns non-executable file attributes as json' do
|
||||||
|
let(:api_user) { inherited_reporter }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
it_behaves_like 'returns non-executable file attributes as json' do
|
||||||
|
let(:api_user) { inherited_developer }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
let_it_be(:project) { create(:project, :private, :repository, group: group) }
|
||||||
|
|
||||||
|
context 'and user is a guest' do
|
||||||
|
it_behaves_like '403 response' do
|
||||||
|
let(:request) { get api(route(file_path), inherited_guest), params: params }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a reporter' do
|
||||||
|
it_behaves_like 'returns non-executable file attributes as json' do
|
||||||
|
let(:api_user) { inherited_reporter }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
it_behaves_like 'returns non-executable file attributes as json' do
|
||||||
|
let(:api_user) { inherited_developer }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /projects/:id/repository/files/:file_path/blame' do
|
describe 'GET /projects/:id/repository/files/:file_path/blame' do
|
||||||
shared_examples_for 'repository blame files' do
|
shared_examples_for 'repository blame files' do
|
||||||
|
@ -406,11 +486,10 @@ RSpec.describe API::Files do
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
|
expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
|
||||||
expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
|
expect(response.headers['X-Gitlab-File-Name']).to eq(file_name)
|
||||||
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
|
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq(last_commit_id)
|
||||||
expect(response.headers['X-Gitlab-Content-Sha256'])
|
expect(response.headers['X-Gitlab-Content-Sha256']).to eq(content_sha256)
|
||||||
.to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
|
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('false')
|
||||||
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("false")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for executable file' do
|
context 'for executable file' do
|
||||||
|
@ -424,13 +503,13 @@ RSpec.describe API::Files do
|
||||||
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
|
expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('6b8dc4a827797aa025ff6b8f425e583858a10d4f')
|
||||||
expect(response.headers['X-Gitlab-Content-Sha256'])
|
expect(response.headers['X-Gitlab-Content-Sha256'])
|
||||||
.to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
|
.to eq('2c74b1181ef780dfb692c030d3a0df6e0b624135c38a9344e56b9f80007b6191')
|
||||||
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq("true")
|
expect(response.headers['X-Gitlab-Execute-Filemode']).to eq('true')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when file path is invalid' do
|
it 'returns 400 when file path is invalid' do
|
||||||
get api(route(rouge_file_path) + '/blame', current_user), params: params
|
get api(route(invalid_file_path) + '/blame', current_user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
|
@ -573,31 +652,35 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is public' do
|
context 'when unauthenticated' do
|
||||||
|
context 'and project is public' do
|
||||||
it_behaves_like 'repository blame files' do
|
it_behaves_like 'repository blame files' do
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:current_user) { nil }
|
let(:current_user) { nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is private' do
|
context 'and project is private' do
|
||||||
it_behaves_like '404 response' do
|
it_behaves_like '404 response' do
|
||||||
let(:request) { get api(route(file_path)), params: params }
|
let(:request) { get api(route(file_path)), params: params }
|
||||||
let(:message) { '404 Project Not Found' }
|
let(:message) { '404 Project Not Found' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a developer' do
|
context 'when authenticated' do
|
||||||
|
context 'and user is a developer' do
|
||||||
it_behaves_like 'repository blame files' do
|
it_behaves_like 'repository blame files' do
|
||||||
let(:current_user) { user }
|
let(:current_user) { user }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a guest' do
|
context 'and user is a guest' do
|
||||||
it_behaves_like '403 response' do
|
it_behaves_like '403 response' do
|
||||||
let(:request) { get api(route(file_path) + '/blame', guest), params: params }
|
let(:request) { get api(route(file_path) + '/blame', guest), params: params }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when PATs are used' do
|
context 'when PATs are used' do
|
||||||
it 'returns blame file by commit sha' do
|
it 'returns blame file by commit sha' do
|
||||||
|
@ -614,10 +697,10 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /projects/:id/repository/files/:file_path/raw" do
|
describe 'GET /projects/:id/repository/files/:file_path/raw' do
|
||||||
shared_examples_for 'repository raw files' do
|
shared_examples_for 'repository raw files' do
|
||||||
it 'returns 400 when file path is invalid' do
|
it 'returns 400 when file path is invalid' do
|
||||||
get api(route(rouge_file_path) + "/raw", current_user), params: params
|
get api(route(invalid_file_path) + '/raw', current_user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
|
@ -628,7 +711,7 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns raw file info' do
|
it 'returns raw file info' do
|
||||||
url = route(file_path) + "/raw"
|
url = route(file_path) + '/raw'
|
||||||
|
|
||||||
expect_to_send_git_blob(api(url, current_user), params)
|
expect_to_send_git_blob(api(url, current_user), params)
|
||||||
end
|
end
|
||||||
|
@ -639,39 +722,39 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns response :ok', :aggregate_failures do
|
it 'returns response :ok', :aggregate_failures do
|
||||||
url = route(file_path) + "/raw"
|
url = route(file_path) + '/raw'
|
||||||
|
|
||||||
expect_to_send_git_blob(api(url, current_user), {})
|
expect_to_send_git_blob(api(url, current_user), {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns raw file info for files with dots' do
|
it 'returns raw file info for files with dots' do
|
||||||
url = route('.gitignore') + "/raw"
|
url = route('.gitignore') + '/raw'
|
||||||
|
|
||||||
expect_to_send_git_blob(api(url, current_user), params)
|
expect_to_send_git_blob(api(url, current_user), params)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns file by commit sha' do
|
it 'returns file by commit sha' do
|
||||||
# This file is deleted on HEAD
|
# This file is deleted on HEAD
|
||||||
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
|
file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
|
||||||
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
|
params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
|
||||||
|
|
||||||
expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params)
|
expect_to_send_git_blob(api(route(file_path) + '/raw', current_user), params)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets no-cache headers' do
|
it 'sets no-cache headers' do
|
||||||
url = route('.gitignore') + "/raw"
|
url = route('.gitignore') + '/raw'
|
||||||
|
|
||||||
expect_to_send_git_blob(api(url, current_user), params)
|
expect_to_send_git_blob(api(url, current_user), params)
|
||||||
|
|
||||||
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
|
expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache')
|
||||||
expect(response.headers["Pragma"]).to eq("no-cache")
|
expect(response.headers['Pragma']).to eq('no-cache')
|
||||||
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
|
expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT')
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when mandatory params are not given' do
|
context 'when mandatory params are not given' do
|
||||||
it_behaves_like '400 response' do
|
it_behaves_like '400 response' do
|
||||||
let(:request) { get api(route("any%2Ffile"), current_user) }
|
let(:request) { get api(route('any%2Ffile'), current_user) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -693,67 +776,88 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is public' do
|
context 'when unauthenticated' do
|
||||||
|
context 'and project is public' do
|
||||||
it_behaves_like 'repository raw files' do
|
it_behaves_like 'repository raw files' do
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:current_user) { nil }
|
let(:current_user) { nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when unauthenticated', 'and project is private' do
|
context 'and project is private' do
|
||||||
it_behaves_like '404 response' do
|
it_behaves_like '404 response' do
|
||||||
let(:request) { get api(route(file_path)), params: params }
|
let(:request) { get api(route(file_path)), params: params }
|
||||||
let(:message) { '404 Project Not Found' }
|
let(:message) { '404 Project Not Found' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a developer' do
|
context 'when authenticated' do
|
||||||
|
context 'and user is a developer' do
|
||||||
it_behaves_like 'repository raw files' do
|
it_behaves_like 'repository raw files' do
|
||||||
let(:current_user) { user }
|
let(:current_user) { user }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated', 'as a guest' do
|
context 'and user is a guest' do
|
||||||
it_behaves_like '403 response' do
|
it_behaves_like '403 response' do
|
||||||
let(:request) { get api(route(file_path), guest), params: params }
|
let(:request) { get api(route(file_path), guest), params: params }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when PATs are used' do
|
context 'when PATs are used' do
|
||||||
it 'returns file by commit sha' do
|
it 'returns file by commit sha' do
|
||||||
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
|
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
|
||||||
|
|
||||||
# This file is deleted on HEAD
|
# This file is deleted on HEAD
|
||||||
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
|
file_path = 'files%2Fjs%2Fcommit%2Ejs%2Ecoffee'
|
||||||
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
|
params[:ref] = '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
|
||||||
url = api(route(file_path) + "/raw", personal_access_token: token)
|
url = api(route(file_path) + '/raw', personal_access_token: token)
|
||||||
|
|
||||||
expect_to_send_git_blob(url, params)
|
expect_to_send_git_blob(url, params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /projects/:id/repository/files/:file_path" do
|
describe 'POST /projects/:id/repository/files/:file_path' do
|
||||||
let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
|
let!(:file_path) { 'new_subfolder%2Fnewfile%2Erb' }
|
||||||
|
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
branch: "master",
|
branch: 'master',
|
||||||
content: "puts 8",
|
content: 'puts 8',
|
||||||
commit_message: "Added newfile"
|
commit_message: 'Added newfile'
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:executable_params) do
|
let(:executable_params) do
|
||||||
{
|
{
|
||||||
branch: "master",
|
branch: 'master',
|
||||||
content: "puts 8",
|
content: 'puts 8',
|
||||||
commit_message: "Added newfile",
|
commit_message: 'Added newfile',
|
||||||
execute_filemode: true
|
execute_filemode: true
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'creates a new file in the project repo' do
|
||||||
|
specify do
|
||||||
|
post api(route(file_path), current_user), params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
|
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
|
||||||
|
last_commit = project.repository.commit.raw
|
||||||
|
expect(last_commit.author_email).to eq(current_user.email)
|
||||||
|
expect(last_commit.author_name).to eq(current_user.name)
|
||||||
|
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authenticated', 'as a direct project member' do
|
||||||
|
context 'when project is private' do
|
||||||
|
context 'and user is a developer' do
|
||||||
it 'returns 400 when file path is invalid' do
|
it 'returns 400 when file path is invalid' do
|
||||||
post api(route(rouge_file_path), user), params: params
|
post api(route(invalid_file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
|
@ -763,53 +867,56 @@ RSpec.describe API::Files do
|
||||||
subject { post api(route(absolute_path), user), params: params }
|
subject { post api(route(absolute_path), user), params: params }
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates a new file in project repo" do
|
it_behaves_like 'creates a new file in the project repo' do
|
||||||
post api(route(file_path), user), params: params
|
let(:current_user) { user }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
|
||||||
expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
|
|
||||||
last_commit = project.repository.commit.raw
|
|
||||||
expect(last_commit.author_email).to eq(user.email)
|
|
||||||
expect(last_commit.author_name).to eq(user.name)
|
|
||||||
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates a new executable file in project repo" do
|
it 'creates a new executable file in project repo' do
|
||||||
post api(route(file_path), user), params: executable_params
|
post api(route(file_path), user), params: executable_params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
|
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
|
||||||
last_commit = project.repository.commit.raw
|
last_commit = project.repository.commit.raw
|
||||||
expect(last_commit.author_email).to eq(user.email)
|
expect(last_commit.author_email).to eq(user.email)
|
||||||
expect(last_commit.author_name).to eq(user.name)
|
expect(last_commit.author_name).to eq(user.name)
|
||||||
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
|
expect(project.repository.blob_at_branch(params[:branch], CGI.unescape(file_path)).executable?).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 400 bad request if no mandatory params given" do
|
context 'when no mandatory params given' do
|
||||||
post api(route("any%2Etxt"), user)
|
it 'returns a 400 bad request' do
|
||||||
|
post api(route('any%2Etxt'), user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns a 400 bad request if the commit message is empty' do
|
context 'when the commit message is empty' do
|
||||||
|
before do
|
||||||
params[:commit_message] = ''
|
params[:commit_message] = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
post api(route(file_path), user), params: params
|
post api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 400 if editor fails to create file" do
|
context 'when editor fails to create file' do
|
||||||
|
before do
|
||||||
allow_next_instance_of(Repository) do |instance|
|
allow_next_instance_of(Repository) do |instance|
|
||||||
allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
|
allow(instance).to receive(:create_file).and_raise(Gitlab::Git::CommitError, 'Cannot create file')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
post api(route("any%2Etxt"), user), params: params
|
it 'returns a 400 bad request' do
|
||||||
|
post api(route('any%2Etxt'), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with PATs' do
|
context 'and PATs are used' do
|
||||||
it 'returns 403 with `read_repository` scope' do
|
it 'returns 403 with `read_repository` scope' do
|
||||||
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
|
token = create(:personal_access_token, scopes: ['read_repository'], user: user)
|
||||||
|
|
||||||
|
@ -827,11 +934,20 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when specifying an author" do
|
context 'and the repo is empty' do
|
||||||
it "creates a new file with the specified author" do
|
let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
|
||||||
|
|
||||||
|
it_behaves_like 'creates a new file in the project repo' do
|
||||||
|
let(:current_user) { user }
|
||||||
|
let(:file_path) { 'newfile%2Erb' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when specifying an author' do
|
||||||
|
it 'creates a new file with the specified author' do
|
||||||
params.merge!(author_email: author_email, author_name: author_name)
|
params.merge!(author_email: author_email, author_name: author_name)
|
||||||
|
|
||||||
post api(route("new_file_with_author%2Etxt"), user), params: params
|
post api(route('new_file_with_author%2Etxt'), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
expect(response.media_type).to eq('application/json')
|
expect(response.media_type).to eq('application/json')
|
||||||
|
@ -840,23 +956,60 @@ RSpec.describe API::Files do
|
||||||
expect(last_commit.author_name).to eq(author_name)
|
expect(last_commit.author_name).to eq(author_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the repo is empty' do
|
|
||||||
let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
|
|
||||||
|
|
||||||
it "creates a new file in project repo" do
|
|
||||||
post api(route("newfile%2Erb"), user), params: params
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
|
||||||
expect(json_response['file_path']).to eq('newfile.rb')
|
|
||||||
last_commit = project.repository.commit.raw
|
|
||||||
expect(last_commit.author_email).to eq(user.email)
|
|
||||||
expect(last_commit.author_name).to eq(user.name)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "PUT /projects/:id/repository/files" do
|
context 'when authenticated' do
|
||||||
|
context 'and user is an inherited member from the group' do
|
||||||
|
context 'when project is public with private repository' do
|
||||||
|
let_it_be(:project) { create(:project, :public, :repository, :repository_private, group: group) }
|
||||||
|
|
||||||
|
context 'and user is a guest' do
|
||||||
|
it_behaves_like '403 response' do
|
||||||
|
let(:request) { post api(route(file_path), inherited_guest), params: params }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a reporter' do
|
||||||
|
it_behaves_like '403 response' do
|
||||||
|
let(:request) { post api(route(file_path), inherited_reporter), params: params }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
it_behaves_like 'creates a new file in the project repo' do
|
||||||
|
let(:current_user) { inherited_developer }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
let_it_be(:project) { create(:project, :private, :repository, group: group) }
|
||||||
|
|
||||||
|
context 'and user is a guest' do
|
||||||
|
it_behaves_like '403 response' do
|
||||||
|
let(:request) { post api(route(file_path), inherited_guest), params: params }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a reporter' do
|
||||||
|
it_behaves_like '403 response' do
|
||||||
|
let(:request) { post api(route(file_path), inherited_reporter), params: params }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
it_behaves_like 'creates a new file in the project repo' do
|
||||||
|
let(:current_user) { inherited_developer }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /projects/:id/repository/files' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
branch: 'master',
|
branch: 'master',
|
||||||
|
@ -865,7 +1018,7 @@ RSpec.describe API::Files do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it "updates existing file in project repo" do
|
it 'updates existing file in project repo' do
|
||||||
put api(route(file_path), user), params: params
|
put api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
@ -875,43 +1028,59 @@ RSpec.describe API::Files do
|
||||||
expect(last_commit.author_name).to eq(user.name)
|
expect(last_commit.author_name).to eq(user.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a 400 bad request if the commit message is empty' do
|
context 'when the commit message is empty' do
|
||||||
|
before do
|
||||||
params[:commit_message] = ''
|
params[:commit_message] = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
put api(route(file_path), user), params: params
|
put api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 400 bad request if update existing file with stale last commit id" do
|
context 'when updating an existing file with stale last commit id' do
|
||||||
params_with_stale_id = params.merge(last_commit_id: 'stale')
|
let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') }
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
put api(route(file_path), user), params: params_with_stale_id
|
put api(route(file_path), user), params: params_with_stale_id
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
|
expect(json_response['message']).to eq(_('You are attempting to update a file that has changed since you started editing it.'))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "updates existing file in project repo with accepts correct last commit id" do
|
context 'with correct last commit id' do
|
||||||
last_commit = Gitlab::Git::Commit
|
let(:last_commit) do
|
||||||
|
Gitlab::Git::Commit
|
||||||
.last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
|
.last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
|
||||||
params_with_correct_id = params.merge(last_commit_id: last_commit.id)
|
end
|
||||||
|
|
||||||
|
let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
|
||||||
|
|
||||||
|
it 'updates existing file in project repo' do
|
||||||
put api(route(file_path), user), params: params_with_correct_id
|
put api(route(file_path), user), params: params_with_correct_id
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "returns 400 when file path is invalid" do
|
context 'when file path is invalid' do
|
||||||
last_commit = Gitlab::Git::Commit
|
let(:last_commit) do
|
||||||
|
Gitlab::Git::Commit
|
||||||
.last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
|
.last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
|
||||||
params_with_correct_id = params.merge(last_commit_id: last_commit.id)
|
end
|
||||||
|
|
||||||
put api(route(rouge_file_path), user), params: params_with_correct_id
|
let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
|
put api(route(invalid_file_path), user), params: params_with_correct_id
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'when path is absolute' do
|
it_behaves_like 'when path is absolute' do
|
||||||
let(:last_commit) do
|
let(:last_commit) do
|
||||||
|
@ -924,15 +1093,17 @@ RSpec.describe API::Files do
|
||||||
subject { put api(route(absolute_path), user), params: params_with_correct_id }
|
subject { put api(route(absolute_path), user), params: params_with_correct_id }
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 400 bad request if no params given" do
|
context 'when no params given' do
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
put api(route(file_path), user)
|
put api(route(file_path), user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when specifying an author" do
|
context 'when specifying an author' do
|
||||||
it "updates a file with the specified author" do
|
it 'updates a file with the specified author' do
|
||||||
params.merge!(author_email: author_email, author_name: author_name, content: "New content")
|
params.merge!(author_email: author_email, author_name: author_name, content: 'New content')
|
||||||
|
|
||||||
put api(route(file_path), user), params: params
|
put api(route(file_path), user), params: params
|
||||||
|
|
||||||
|
@ -982,7 +1153,7 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "DELETE /projects/:id/repository/files" do
|
describe 'DELETE /projects/:id/repository/files' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
branch: 'master',
|
branch: 'master',
|
||||||
|
@ -991,7 +1162,7 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when file path is invalid' do
|
it 'returns 400 when file path is invalid' do
|
||||||
delete api(route(rouge_file_path), user), params: params
|
delete api(route(invalid_file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response['error']).to eq(invalid_file_message)
|
expect(json_response['error']).to eq(invalid_file_message)
|
||||||
|
@ -1001,38 +1172,48 @@ RSpec.describe API::Files do
|
||||||
subject { delete api(route(absolute_path), user), params: params }
|
subject { delete api(route(absolute_path), user), params: params }
|
||||||
end
|
end
|
||||||
|
|
||||||
it "deletes existing file in project repo" do
|
it 'deletes existing file in project repo' do
|
||||||
delete api(route(file_path), user), params: params
|
delete api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:no_content)
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 400 bad request if no params given" do
|
context 'when no params given' do
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
delete api(route(file_path), user)
|
delete api(route(file_path), user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns a 400 bad request if the commit message is empty' do
|
context 'when the commit message is empty' do
|
||||||
|
before do
|
||||||
params[:commit_message] = ''
|
params[:commit_message] = ''
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
delete api(route(file_path), user), params: params
|
delete api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 400 if fails to delete file" do
|
context 'when fails to delete file' do
|
||||||
|
before do
|
||||||
allow_next_instance_of(Repository) do |instance|
|
allow_next_instance_of(Repository) do |instance|
|
||||||
allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
|
allow(instance).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a 400 bad request' do
|
||||||
delete api(route(file_path), user), params: params
|
delete api(route(file_path), user), params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when specifying an author" do
|
context 'when specifying an author' do
|
||||||
it "removes a file with the specified author" do
|
it 'removes a file with the specified author' do
|
||||||
params.merge!(author_email: author_email, author_name: author_name)
|
params.merge!(author_email: author_email, author_name: author_name)
|
||||||
|
|
||||||
delete api(route(file_path), user), params: params
|
delete api(route(file_path), user), params: params
|
||||||
|
@ -1042,7 +1223,7 @@ RSpec.describe API::Files do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /projects/:id/repository/files with binary file" do
|
describe 'POST /projects/:id/repository/files with binary file' do
|
||||||
let(:file_path) { 'test%2Ebin' }
|
let(:file_path) { 'test%2Ebin' }
|
||||||
let(:put_params) do
|
let(:put_params) do
|
||||||
{
|
{
|
||||||
|
@ -1063,7 +1244,7 @@ RSpec.describe API::Files do
|
||||||
post api(route(file_path), user), params: put_params
|
post api(route(file_path), user), params: put_params
|
||||||
end
|
end
|
||||||
|
|
||||||
it "remains unchanged" do
|
it 'remains unchanged' do
|
||||||
get api(route(file_path), user), params: get_params
|
get api(route(file_path), user), params: get_params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
|
|
@ -5,26 +5,18 @@ require 'spec_helper'
|
||||||
RSpec.describe 'Creation of a new branch' do
|
RSpec.describe 'Creation of a new branch' do
|
||||||
include GraphqlHelpers
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be(:group) { create(:group, :public) }
|
||||||
let_it_be(:current_user) { create(:user) }
|
let_it_be(:current_user) { create(:user) }
|
||||||
let_it_be(:project) { create(:project, :public, :empty_repo) }
|
|
||||||
|
|
||||||
let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } }
|
let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } }
|
||||||
let(:new_branch) { 'new_branch' }
|
let(:new_branch) { "new_branch_#{SecureRandom.hex(4)}" }
|
||||||
let(:ref) { 'master' }
|
let(:ref) { 'master' }
|
||||||
|
|
||||||
let(:mutation) { graphql_mutation(:create_branch, input) }
|
let(:mutation) { graphql_mutation(:create_branch, input) }
|
||||||
let(:mutation_response) { graphql_mutation_response(:create_branch) }
|
let(:mutation_response) { graphql_mutation_response(:create_branch) }
|
||||||
|
|
||||||
context 'the user is not allowed to create a branch' do
|
shared_examples 'creates a new branch' do
|
||||||
it_behaves_like 'a mutation that returns a top-level access error'
|
specify do
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user has permissions to create a branch' do
|
|
||||||
before do
|
|
||||||
project.add_developer(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a new branch' do
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
post_graphql_mutation(mutation, current_user: current_user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:success)
|
expect(response).to have_gitlab_http_status(:success)
|
||||||
|
@ -33,14 +25,75 @@ RSpec.describe 'Creation of a new branch' do
|
||||||
'commit' => a_hash_including('id')
|
'commit' => a_hash_including('id')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public' do
|
||||||
|
let_it_be(:project) { create(:project, :public, :empty_repo) }
|
||||||
|
|
||||||
|
context 'when user is not allowed to create a branch' do
|
||||||
|
it_behaves_like 'a mutation that returns a top-level access error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is a direct project member' do
|
||||||
|
context 'and user is a developer' do
|
||||||
|
before do
|
||||||
|
project.add_developer(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates a new branch'
|
||||||
|
|
||||||
context 'when ref is not correct' do
|
context 'when ref is not correct' do
|
||||||
err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
|
err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\''
|
||||||
let(:new_branch) { 'another_branch' }
|
let(:new_branch) { 'another_branch' }
|
||||||
let(:ref) { 'unknown' }
|
let(:ref) { 'unknown' }
|
||||||
|
|
||||||
it_behaves_like 'a mutation that returns errors in the response',
|
it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg]
|
||||||
errors: [err_msg]
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is an inherited member from the group' do
|
||||||
|
context 'when project has a private repository' do
|
||||||
|
let_it_be(:project) { create(:project, :public, :empty_repo, :repository_private, group: group) }
|
||||||
|
|
||||||
|
context 'and user is a guest' do
|
||||||
|
before do
|
||||||
|
group.add_guest(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a mutation that returns a top-level access error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
before do
|
||||||
|
group.add_developer(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates a new branch'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
let_it_be(:project) { create(:project, :private, :empty_repo, group: group) }
|
||||||
|
|
||||||
|
context 'when user is an inherited member from the group' do
|
||||||
|
context 'and user is a guest' do
|
||||||
|
before do
|
||||||
|
group.add_guest(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a mutation that returns a top-level access error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user is a developer' do
|
||||||
|
before do
|
||||||
|
group.add_developer(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates a new branch'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -923,6 +923,16 @@ RSpec.describe API::Groups do
|
||||||
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
|
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'removes the group avatar' do
|
||||||
|
put api("/groups/#{group1.id}", user1), params: { avatar: '' }
|
||||||
|
|
||||||
|
aggregate_failures "testing response" do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['avatar_url']).to be_nil
|
||||||
|
expect(group1.reload.avatar_url).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not update visibility_level if it is restricted' do
|
it 'does not update visibility_level if it is restricted' do
|
||||||
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||||
|
|
||||||
|
|
|
@ -573,6 +573,224 @@ RSpec.describe API::Releases do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET /projects/:id/releases/:tag_name/downloads/*file_path' do
|
||||||
|
let!(:release) { create(:release, project: project, tag: 'v0.1', author: maintainer) }
|
||||||
|
let!(:link) { create(:release_link, release: release, url: "#{url}#{filepath}", filepath: filepath) }
|
||||||
|
let(:filepath) { '/bin/bigfile.exe' }
|
||||||
|
let(:url) { 'https://google.com/-/jobs/140463678/artifacts/download' }
|
||||||
|
|
||||||
|
context 'with an invalid release tag' do
|
||||||
|
it 'returns 404 for maintater' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", maintainer)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(json_response['message']).to eq('404 Not Found')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns project not found for no user' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", nil)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(json_response['message']).to eq('404 Project Not Found')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns forbidden for guest' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.2/downloads#{filepath}", guest)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a valid release tag' do
|
||||||
|
context 'when filepath is provided' do
|
||||||
|
context 'when filepath exists' do
|
||||||
|
it 'redirects to the file download URL' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", maintainer)
|
||||||
|
|
||||||
|
expect(response).to redirect_to("#{url}#{filepath}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to the file download URL when using JOB-TOKEN auth' do
|
||||||
|
job = create(:ci_build, :running, project: project, user: maintainer)
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}"), params: { job_token: job.token }
|
||||||
|
|
||||||
|
expect(response).to redirect_to("#{url}#{filepath}")
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is a guest' do
|
||||||
|
it 'responds 403 Forbidden' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is public' do
|
||||||
|
let(:project) { create(:project, :repository, :public) }
|
||||||
|
|
||||||
|
it 'responds 200 OK' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads#{filepath}", guest)
|
||||||
|
|
||||||
|
expect(response).to redirect_to("#{url}#{filepath}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when filepath does not exists' do
|
||||||
|
it 'returns 404 for maintater' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", maintainer)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(json_response['message']).to eq('404 Not found')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns project not found for no user' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", nil)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(json_response['message']).to eq('404 Project Not Found')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns forbidden for guest' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads/bin/not_existing.exe", guest)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when filepath is not provided' do
|
||||||
|
it 'returns 404 for maintater' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads", maintainer)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns project not found for no user' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads", nil)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns forbidden for guest' do
|
||||||
|
get api("/projects/#{project.id}/releases/v0.1/downloads", guest)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /projects/:id/releases/permalink/latest' do
|
||||||
|
context 'when there is no release' do
|
||||||
|
it 'returns not found' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns not found when using JOB-TOKEN auth' do
|
||||||
|
job = create(:ci_build, :running, project: project, user: maintainer)
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are more than one release' do
|
||||||
|
let!(:release_a) do
|
||||||
|
create(:release,
|
||||||
|
project: project,
|
||||||
|
tag: 'v0.1',
|
||||||
|
author: maintainer,
|
||||||
|
description: 'This is v0.1',
|
||||||
|
released_at: 3.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:release_b) do
|
||||||
|
create(:release,
|
||||||
|
project: project,
|
||||||
|
tag: 'v0.2',
|
||||||
|
author: maintainer,
|
||||||
|
description: 'This is v0.2',
|
||||||
|
released_at: 2.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to the latest release tag' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest", maintainer)
|
||||||
|
|
||||||
|
uri = URI(response.header["Location"])
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to the latest release tag when using JOB-TOKEN auth' do
|
||||||
|
job = create(:ci_build, :running, project: project, user: maintainer)
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest"), params: { job_token: job.token }
|
||||||
|
|
||||||
|
uri = URI(response.header["Location"])
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are query parameters present' do
|
||||||
|
it 'includes the query params on the redirection' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { include_html_description: true, other_param: "aaa" }
|
||||||
|
|
||||||
|
uri = URI(response.header["Location"])
|
||||||
|
query_params = Rack::Utils.parse_nested_query(uri.query)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
|
||||||
|
expect(query_params).to include({
|
||||||
|
"include_html_description" => "true",
|
||||||
|
"other_param" => "aaa"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'discards the `order_by` query param' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest", maintainer), params: { order_by: 'something', other_param: "aaa" }
|
||||||
|
|
||||||
|
uri = URI(response.header["Location"])
|
||||||
|
query_params = Rack::Utils.parse_nested_query(uri.query)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}")
|
||||||
|
expect(query_params).to include({
|
||||||
|
"other_param" => "aaa"
|
||||||
|
})
|
||||||
|
expect(query_params).not_to include({
|
||||||
|
"order_by" => "something"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when downloading a release asset' do
|
||||||
|
it 'redirects to the right endpoint keeping the suffix_path' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/example.exe", maintainer)
|
||||||
|
|
||||||
|
uri = URI(response.header["Location"])
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(uri.path).to eq("/api/v4/projects/#{project.id}/releases/#{release_b.tag}/downloads/bin/example.exe")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error when there is path traversal in suffix path' do
|
||||||
|
get api("/projects/#{project.id}/releases/permalink/latest/downloads/bin/../../../../../../../password.txt", maintainer)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
|
||||||
|
expect(json_response['error']).to eq('suffix_path should be a valid file path')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST /projects/:id/releases' do
|
describe 'POST /projects/:id/releases' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
|
|
|
@ -201,6 +201,25 @@ RSpec.describe Environments::StopService do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'and merge request has associated created_environments' do
|
||||||
|
let!(:environment1) { create(:environment, project: project, merge_request: merge_request) }
|
||||||
|
let!(:environment2) { create(:environment, project: project, merge_request: merge_request) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stops the associated created_environments' do
|
||||||
|
expect(environment1.reload).to be_stopped
|
||||||
|
expect(environment2.reload).to be_stopped
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not affect environments that are not associated to the merge request' do
|
||||||
|
expect(pipeline.environments_in_self_and_project_descendants.first.merge_request).to be_nil
|
||||||
|
expect(pipeline.environments_in_self_and_project_descendants.first).to be_available
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'stops the active environment' do
|
it 'stops the active environment' do
|
||||||
subject
|
subject
|
||||||
expect(pipeline.environments_in_self_and_project_descendants.first).to be_stopping
|
expect(pipeline.environments_in_self_and_project_descendants.first).to be_stopping
|
||||||
|
|
|
@ -218,7 +218,6 @@
|
||||||
- './ee/spec/elastic/migrate/20220713103500_delete_commits_from_original_index_spec.rb'
|
- './ee/spec/elastic/migrate/20220713103500_delete_commits_from_original_index_spec.rb'
|
||||||
- './ee/spec/factories/lfs_object_spec.rb'
|
- './ee/spec/factories/lfs_object_spec.rb'
|
||||||
- './ee/spec/features/account_recovery_regular_check_spec.rb'
|
- './ee/spec/features/account_recovery_regular_check_spec.rb'
|
||||||
- './ee/spec/features/admin/admin_audit_logs_spec.rb'
|
|
||||||
- './ee/spec/features/admin/admin_credentials_inventory_spec.rb'
|
- './ee/spec/features/admin/admin_credentials_inventory_spec.rb'
|
||||||
- './ee/spec/features/admin/admin_dashboard_spec.rb'
|
- './ee/spec/features/admin/admin_dashboard_spec.rb'
|
||||||
- './ee/spec/features/admin/admin_dev_ops_reports_spec.rb'
|
- './ee/spec/features/admin/admin_dev_ops_reports_spec.rb'
|
||||||
|
|
Loading…
Reference in New Issue