diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 29318d6aaa8..29edd18cef2 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -135,8 +135,11 @@ export default {
:label-for="fieldId"
:invalid-feedback="__('This field is required.')"
:state="valid"
- :description="help"
>
+
+
+
+
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index 3c1036bbd34..cf3c765a0ec 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -54,9 +54,8 @@ module IntegrationsActions
end
def integration
- # Using instance variable `@service` still required as it's used in ServiceParams
- # and app/views/shared/_service_settings.html.haml. Should be removed once
- # those 2 are refactored to use `@integration`.
+ # Using instance variable `@service` still required as it's used in ServiceParams.
+ # Should be removed once that is refactored to use `@integration`.
@integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 8846ff54eb2..7c7cd87a7c1 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -23,6 +23,7 @@
# min_access_level: integer
# last_activity_after: datetime
# last_activity_before: datetime
+# repository_storage: string
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
@@ -75,6 +76,7 @@ class ProjectsFinder < UnionFinder
collection = by_deleted_status(collection)
collection = by_last_activity_after(collection)
collection = by_last_activity_before(collection)
+ collection = by_repository_storage(collection)
collection
end
@@ -197,6 +199,14 @@ class ProjectsFinder < UnionFinder
end
end
+ def by_repository_storage(items)
+ if params[:repository_storage].present?
+ items.where(repository_storage: params[:repository_storage]) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ items
+ end
+ end
+
def sort(items)
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index fe839b92ba6..a9d70522d9b 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -44,13 +44,6 @@ module ServicesHelper
end
end
- def event_action_description(action)
- case action
- when "comment"
- s_("ProjectService|Comment will be posted on each event")
- end
- end
-
def service_save_button
button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
@@ -90,7 +83,7 @@ module ServicesHelper
def scoped_test_integration_path(integration)
if @project.present?
- test_project_settings_integration_path(@project, integration)
+ test_project_service_path(@project, integration)
elsif @group.present?
test_group_settings_integration_path(@group, integration)
else
@@ -102,22 +95,36 @@ module ServicesHelper
Feature.enabled?(:integration_form_refactor, @project)
end
- def trigger_events_for_service
- return [] unless integration_form_refactor?
-
- ServiceEventSerializer.new(service: @service).represent(@service.configurable_events).to_json
+ def integration_form_data(integration)
+ {
+ show_active: integration.show_active_box?.to_s,
+ activated: (integration.active || integration.new_record?).to_s,
+ type: integration.to_param,
+ merge_request_events: integration.merge_requests_events.to_s,
+ commit_events: integration.commit_events.to_s,
+ enable_comments: integration.comment_on_event_enabled.to_s,
+ comment_detail: integration.comment_detail,
+ trigger_events: trigger_events_for_service(integration),
+ fields: fields_for_service(integration)
+ }
end
- def fields_for_service
+ def trigger_events_for_service(integration)
return [] unless integration_form_refactor?
- ServiceFieldSerializer.new(service: @service).represent(@service.global_fields).to_json
+ ServiceEventSerializer.new(service: integration).represent(integration.configurable_events).to_json
end
- def show_service_trigger_events?
- return false if @service.is_a?(JiraService) || integration_form_refactor?
+ def fields_for_service(integration)
+ return [] unless integration_form_refactor?
- @service.configurable_events.present?
+ ServiceFieldSerializer.new(service: integration).represent(integration.global_fields).to_json
+ end
+
+ def show_service_trigger_events?(integration)
+ return false if integration.is_a?(JiraService) || integration_form_refactor?
+
+ integration.configurable_events.present?
end
extend self
diff --git a/app/models/project.rb b/app/models/project.rb
index f46583d1d98..e63eac02dca 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1925,6 +1925,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
+ .append(key: 'CI_PROJECT_ROOT_NAMESPACE', value: namespace.root_ancestor.path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
.append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 03f5a863421..c66f0d199b0 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -105,6 +105,9 @@ class GlobalPolicy < BasePolicy
enable :update_custom_attribute
end
+ # We can't use `read_statistics` because the user may have different permissions for different projects
+ rule { admin }.enable :use_project_statistics_filters
+
rule { external_user }.prevent :create_snippet
end
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index d9ce2c5e905..8e72a185406 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -80,7 +80,7 @@ module Metrics
def fetch_dashboard
uid = GrafanaUidParser.new(grafana_url, project).parse
- raise DashboardProcessingError.new('Dashboard uid not found') unless uid
+ raise DashboardProcessingError.new(_('Dashboard uid not found')) unless uid
response = client.get_dashboard(uid: uid)
@@ -89,7 +89,7 @@ module Metrics
def fetch_datasource(dashboard)
name = DatasourceNameParser.new(grafana_url, dashboard).parse
- raise DashboardProcessingError.new('Datasource name not found') unless name
+ raise DashboardProcessingError.new(_('Datasource name not found')) unless name
response = client.get_datasource(name: name)
@@ -115,7 +115,7 @@ module Metrics
def parse_json(json)
Gitlab::Json.parse(json, symbolize_names: true)
rescue JSON::ParserError
- raise DashboardProcessingError.new('Grafana response contains invalid json')
+ raise DashboardProcessingError.new(_('Grafana response contains invalid json'))
end
end
diff --git a/app/services/metrics/dashboard/transient_embed_service.rb b/app/services/metrics/dashboard/transient_embed_service.rb
index 9dd1d25692c..0a9c4bc7b86 100644
--- a/app/services/metrics/dashboard/transient_embed_service.rb
+++ b/app/services/metrics/dashboard/transient_embed_service.rb
@@ -39,7 +39,7 @@ module Metrics
end
def invalid_embed_json!(message)
- raise DashboardProcessingError.new("Parsing error for param :embed_json. #{message}")
+ raise DashboardProcessingError.new(_("Parsing error for param :embed_json. %{message}") % { message: message })
end
end
end
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index d18e91c0b14..f2153e503af 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -4,7 +4,7 @@
%p #{@service.description} template.
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
- = render 'shared/service_settings', form: form, service: @service
+ = render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block
= form.submit 'Save', class: 'btn btn-success'
diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml
index 00ed5464a44..558fe3ab15d 100644
--- a/app/views/admin/services/edit.html.haml
+++ b/app/views/admin/services/edit.html.haml
@@ -1,5 +1,6 @@
- add_to_breadcrumbs "Service Templates", admin_application_settings_services_path
- breadcrumb_title @service.title
- page_title @service.title, "Service Templates"
+- @content_class = 'limit-container-width' unless fluid_layout
= render 'form'
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 28e52dc85db..f40b0d74a1a 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -216,7 +216,7 @@
%strong.fly-out-top-item-name
= _('Appearance')
- = nav_link(controller: :application_settings) do
+ = nav_link(controller: [:application_settings, :integrations]) do
= link_to general_admin_application_settings_path do
.nav-icon-container
= sprite_icon('settings')
@@ -224,7 +224,7 @@
= _('Settings')
%ul.sidebar-sub-level-items.qa-admin-sidebar-settings-submenu
- = nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:application_settings, :integrations], html_options: { class: "fly-out-top-item" } ) do
= link_to general_admin_application_settings_path do
%strong.fly-out-top-item-name
= _('Settings')
@@ -233,7 +233,7 @@
= link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
%span
= _('General')
- = nav_link(path: 'application_settings#integrations') do
+ = nav_link(path: ['application_settings#integrations', 'integrations#edit']) do
= link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do
%span
= _('Integrations')
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index e6761807409..91aa148dcb3 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -11,7 +11,7 @@
%p= @service.detailed_description
.col-lg-8
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
- = render 'shared/service_settings', form: form, service: @service
+ = render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 92b86c6fec1..b144f59d1fc 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -1,22 +1,21 @@
-= form_errors(@service)
+= form_errors(integration)
-- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
- = render "projects/services/#{@service.to_param}/help", subject: @service
-- elsif @service.help.present?
+- if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
+ = render "projects/services/#{integration.to_param}/help", subject: integration
+- elsif integration.help.present?
.info-well
.well-segment
- = markdown @service.help
+ = markdown integration.help
.service-settings
- .js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, type: @service.to_param, merge_request_events: @service.merge_requests_events.to_s,
-commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on_event_enabled.to_s, comment_detail: @service.comment_detail, trigger_events: trigger_events_for_service, fields: fields_for_service } }
+ .js-vue-integration-settings{ data: integration_form_data(integration) }
- - if show_service_trigger_events?
+ - if show_service_trigger_events?(integration)
.form-group.row
%label.col-form-label.col-sm-2= _('Trigger')
.col-sm-10
- - @service.configurable_events.each do |event|
+ - integration.configurable_events.each do |event|
.form-group
.form-check
= form.check_box service_event_field_name(event), class: 'form-check-input'
@@ -24,14 +23,14 @@ commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on
%strong
= event.humanize
- - field = @service.event_field(event)
+ - field = integration.event_field(event)
- if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.text-muted
- = @service.class.event_description(event)
+ = integration.class.event_description(event)
- unless integration_form_refactor?
- - @service.global_fields.each do |field|
+ - integration.global_fields.each do |field|
= render 'shared/field', form: form, field: field
diff --git a/app/views/shared/integrations/edit.html.haml b/app/views/shared/integrations/edit.html.haml
index 927d2410132..a996f72e2f4 100644
--- a/app/views/shared/integrations/edit.html.haml
+++ b/app/views/shared/integrations/edit.html.haml
@@ -1,5 +1,6 @@
- add_to_breadcrumbs _('Integrations'), scoped_integrations_path
- breadcrumb_title @integration.title
- page_title @integration.title, _('Integrations')
+- @content_class = 'limit-container-width' unless fluid_layout
= render 'shared/integrations/form', integration: @integration
diff --git a/changelogs/unreleased/justin_ho-refine-ui-of-integration-form.yml b/changelogs/unreleased/justin_ho-refine-ui-of-integration-form.yml
new file mode 100644
index 00000000000..aa8a5d66703
--- /dev/null
+++ b/changelogs/unreleased/justin_ho-refine-ui-of-integration-form.yml
@@ -0,0 +1,5 @@
+---
+title: Refine UI of integration form
+merge_request: 34707
+author:
+type: changed
diff --git a/changelogs/unreleased/nfriend-add-CI_ROOT_NAMESPACE-env-variable.yml b/changelogs/unreleased/nfriend-add-CI_ROOT_NAMESPACE-env-variable.yml
new file mode 100644
index 00000000000..5ebee05975f
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-CI_ROOT_NAMESPACE-env-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI_PROJECT_ROOT_NAMESPACE predefined environment variable
+merge_request: 34733
+author:
+type: added
diff --git a/changelogs/unreleased/projects-api-sort-by-statistics.yml b/changelogs/unreleased/projects-api-sort-by-statistics.yml
new file mode 100644
index 00000000000..e46c4a53cc5
--- /dev/null
+++ b/changelogs/unreleased/projects-api-sort-by-statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Allow advanced API projects filtering for admins
+merge_request: 32879
+author:
+type: added
diff --git a/changelogs/unreleased/sh-fix-gitaly-blob-service-durations.yml b/changelogs/unreleased/sh-fix-gitaly-blob-service-durations.yml
new file mode 100644
index 00000000000..4c844ccb6ef
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-gitaly-blob-service-durations.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Gitaly duration timings of BlobService RPCs
+merge_request: 34906
+author:
+type: other
diff --git a/db/migrate/20200605160806_add_index_on_repository_size_and_project_id_to_project_statistics.rb b/db/migrate/20200605160806_add_index_on_repository_size_and_project_id_to_project_statistics.rb
new file mode 100644
index 00000000000..2e7b75bcd17
--- /dev/null
+++ b/db/migrate/20200605160806_add_index_on_repository_size_and_project_id_to_project_statistics.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnRepositorySizeAndProjectIdToProjectStatistics < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_statistics, [:repository_size, :project_id]
+ end
+
+ def down
+ remove_concurrent_index :project_statistics, [:repository_size, :project_id]
+ end
+end
diff --git a/db/migrate/20200605160836_add_index_on_storage_size_and_project_id_to_project_statistics.rb b/db/migrate/20200605160836_add_index_on_storage_size_and_project_id_to_project_statistics.rb
new file mode 100644
index 00000000000..22f9dab634b
--- /dev/null
+++ b/db/migrate/20200605160836_add_index_on_storage_size_and_project_id_to_project_statistics.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnStorageSizeAndProjectIdToProjectStatistics < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_statistics, [:storage_size, :project_id]
+ end
+
+ def down
+ remove_concurrent_index :project_statistics, [:storage_size, :project_id]
+ end
+end
diff --git a/db/migrate/20200605160851_add_index_on_wiki_size_and_project_id_to_project_statistics.rb b/db/migrate/20200605160851_add_index_on_wiki_size_and_project_id_to_project_statistics.rb
new file mode 100644
index 00000000000..d32994afbfc
--- /dev/null
+++ b/db/migrate/20200605160851_add_index_on_wiki_size_and_project_id_to_project_statistics.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnWikiSizeAndProjectIdToProjectStatistics < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_statistics, [:wiki_size, :project_id]
+ end
+
+ def down
+ remove_concurrent_index :project_statistics, [:wiki_size, :project_id]
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 1be04406542..02540be4e11 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10643,6 +10643,12 @@ CREATE INDEX index_project_statistics_on_namespace_id ON public.project_statisti
CREATE UNIQUE INDEX index_project_statistics_on_project_id ON public.project_statistics USING btree (project_id);
+CREATE INDEX index_project_statistics_on_repository_size_and_project_id ON public.project_statistics USING btree (repository_size, project_id);
+
+CREATE INDEX index_project_statistics_on_storage_size_and_project_id ON public.project_statistics USING btree (storage_size, project_id);
+
+CREATE INDEX index_project_statistics_on_wiki_size_and_project_id ON public.project_statistics USING btree (wiki_size, project_id);
+
CREATE UNIQUE INDEX index_project_tracing_settings_on_project_id ON public.project_tracing_settings USING btree (project_id);
CREATE INDEX index_projects_api_created_at_id_desc ON public.projects USING btree (created_at, id DESC);
@@ -14055,6 +14061,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200604174558
20200605003204
20200605093113
+20200605160806
+20200605160836
+20200605160851
20200608072931
20200608075553
20200608214008
diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md
index 9e87eed4508..5aadbf75c5d 100644
--- a/doc/administration/feature_flags.md
+++ b/doc/administration/feature_flags.md
@@ -103,10 +103,10 @@ Some feature flags can be enabled or disabled on a per project basis:
Feature.enable(:, Project.find())
```
-For example, to enable the [`:release_evidence_collection`](../ci/junit_test_reports.md#enabling-the-feature) feature flag for project `1234`:
+For example, to enable the [`:junit_pipeline_view`](../ci/junit_test_reports.md#enabling-the-junit-test-reports-feature-core-only) feature flag for project `1234`:
```ruby
-Feature.enable(:release_evidence_collection, Project.find(1234))
+Feature.enable(:junit_pipeline_view, Project.find(1234))
```
When the feature is ready, GitLab will remove the feature flag, the option for
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index e84d7663bdb..563829b8192 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -142,7 +142,7 @@ Example of response
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202525) in GitLab 13.0.
CAUTION: **Caution:**
-This API route is part of the [JUnit test report](../ci/junit_test_reports.md) feature. It is protected by a [feature flag](../development/feature_flags/index.md) that is **disabled** due to performance issues with very large data sets. See [the documentation for the feature](../ci/junit_test_reports.md#enabling-the-feature) for further details.
+This API route is part of the [JUnit test report](../ci/junit_test_reports.md) feature. It is protected by a [feature flag](../development/feature_flags/index.md) that is **disabled** due to performance issues with very large data sets.
```plaintext
GET /projects/:id/pipelines/:pipeline_id/test_report
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b9ba632cd9e..1601151f708 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -45,7 +45,7 @@ GET /projects
| --------- | ---- | -------- | ----------- |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
-| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. `repository_size`, `storage_size`, or `wiki_size` fields are only allowed for admins. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria |
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` |
@@ -65,6 +65,7 @@ GET /projects
| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID |
| `last_activity_after` | datetime | no | Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
| `last_activity_before` | datetime | no | Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
+| `repository_storage` | string | no | Limit results to projects stored on repository_storage. Available for admins only. |
NOTE: **Note:**
This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options.
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index aa0d40a4d06..0ddd1a0e656 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -206,7 +206,7 @@ cunit:
junit: ./my-cunit-test.xml
```
-### .Net example
+### .NET example
The [JunitXML.TestLogger](https://www.nuget.org/packages/JunitXml.TestLogger/) NuGet
package can generate test reports for .Net Framework and .Net Core applications. The following
@@ -234,7 +234,9 @@ Test:
## Viewing JUnit test reports on GitLab
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24792) in GitLab 12.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24792) in GitLab 12.5.
+> - It's deployed behind a feature flag, disabled by default.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enabling-the-junit-test-reports-feature-core-only). **(CORE ONLY)**
If JUnit XML files are generated and uploaded as part of a pipeline, these reports
can be viewed inside the pipelines details page. The **Tests** tab on this page will
@@ -248,7 +250,7 @@ with failed showing at the top, skipped next and successful cases last.
You can also retrieve the reports via the [GitLab API](../api/pipelines.md#get-a-pipelines-test-report).
-### Enabling the feature
+### Enabling the JUnit test reports feature **(CORE ONLY)**
This feature comes with the `:junit_pipeline_view` feature flag disabled by default. This
feature is disabled due to some performance issues with very large data sets.
@@ -266,7 +268,9 @@ Feature.enable(:junit_pipeline_view, Project.find())
## Viewing JUnit screenshots on GitLab
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
+> - It's deployed behind a feature flag, disabled by default.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enabling-the-junit-screenshots-feature-core-only). **(CORE ONLY)**
If JUnit XML files contain an `attachment` tag, GitLab parses the attachment.
@@ -280,7 +284,7 @@ Upload your screenshots as [artifacts](pipelines/job_artifacts.md#artifactsrepor
When [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/6061) is complete, the attached file will be visible on the pipeline details page.
-### Enabling the feature
+### Enabling the JUnit screenshots feature **(CORE ONLY)**
This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 541e739082f..6221d020484 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -341,6 +341,7 @@ export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-foss"
export CI_PROJECT_NAME="gitlab-foss"
export CI_PROJECT_TITLE="GitLab FOSS"
export CI_PROJECT_NAMESPACE="gitlab-org"
+export CI_PROJECT_ROOT_NAMESPACE="gitlab-org"
export CI_PROJECT_PATH="gitlab-org/gitlab-foss"
export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-foss"
export CI_REGISTRY="registry.example.com"
@@ -909,6 +910,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_PROJECT_PATH_SLUG=gitlab-examples-ci-debug-trace
++ export CI_PROJECT_NAMESPACE=gitlab-examples
++ CI_PROJECT_NAMESPACE=gitlab-examples
+++ export CI_PROJECT_ROOT_NAMESPACE=gitlab-examples
+++ CI_PROJECT_ROOT_NAMESPACE=gitlab-examples
++ export CI_PROJECT_URL=https://gitlab.com/gitlab-examples/ci-debug-trace
++ CI_PROJECT_URL=https://gitlab.com/gitlab-examples/ci-debug-trace
++ export CI_PROJECT_VISIBILITY=public
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index daadae7c5d9..c6cf33e5610 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -99,6 +99,7 @@ You can add a command to your `.gitlab-ci.yml` file to
| `CI_PROJECT_ID` | all | all | The unique ID of the current project that GitLab CI/CD uses internally |
| `CI_PROJECT_NAME` | 8.10 | 0.5 | The name of the directory for the project that is currently being built. For example, if the project URL is `gitlab.example.com/group-name/project-1`, the `CI_PROJECT_NAME` would be `project-1`. |
| `CI_PROJECT_NAMESPACE` | 8.10 | 0.5 | The project namespace (username or group name) that is currently being built |
+| `CI_PROJECT_ROOT_NAMESPACE` | 13.2 | 0.5 | The **root** project namespace (username or group name) that is currently being built. For example, if `CI_PROJECT_NAME` is `root-group/child-group/grandchild-group`, `CI_PROJECT_ROOT_NAMESPACE` would be `root-group`. |
| `CI_PROJECT_PATH` | 8.10 | 0.5 | The namespace with project name |
| `CI_PROJECT_PATH_SLUG` | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| `CI_PROJECT_REPOSITORY_LANGUAGES` | 12.3 | all | Comma-separated, lowercased list of the languages used in the repository (e.g. `ruby,javascript,html,css`) |
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index bbdb45da3b1..b1bbc3cf0fd 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -543,6 +543,7 @@ module API
finder_params[:id_before] = params[:id_before] if params[:id_before]
finder_params[:last_activity_after] = params[:last_activity_after] if params[:last_activity_after]
finder_params[:last_activity_before] = params[:last_activity_before] if params[:last_activity_before]
+ finder_params[:repository_storage] = params[:repository_storage] if params[:repository_storage]
finder_params
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 8a115d42929..92030415d02 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -6,6 +6,8 @@ module API
extend ActiveSupport::Concern
extend Grape::API::Helpers
+ STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze
+
params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project'
optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`'
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index e00fb61f478..f4582d36f4b 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -17,6 +17,7 @@ module API
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
+ projects = projects.joins(:statistics) if params[:order_by].include?('project_statistics') # rubocop: disable CodeReuse/ActiveRecord
lang = params[:with_programming_language]
projects = projects.with_programming_language(lang) if lang
@@ -28,6 +29,20 @@ module API
attrs.delete(:repository_storage) unless can?(current_user, :change_repository_storage, project)
end
+ def verify_project_filters!(attrs)
+ attrs.delete(:repository_storage) unless can?(current_user, :use_project_statistics_filters)
+ end
+
+ def verify_statistics_order_by_projects!
+ return unless Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.include?(params[:order_by])
+
+ params[:order_by] = if can?(current_user, :use_project_statistics_filters)
+ "project_statistics.#{params[:order_by]}"
+ else
+ route.params['order_by'][:default]
+ end
+ end
+
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
@@ -52,8 +67,9 @@ module API
end
params :sort_params do
- optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
- default: 'created_at', desc: 'Return projects ordered by field'
+ optional :order_by, type: String,
+ values: %w[id name path created_at updated_at last_activity_at] + Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS,
+ default: 'created_at', desc: "Return projects ordered by field. #{Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.join(', ')} are only available to admins."
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order'
end
@@ -75,6 +91,7 @@ module API
optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID'
optional :last_activity_after, type: DateTime, desc: 'Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
optional :last_activity_before, type: DateTime, desc: 'Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
use :optional_filter_params_ee
end
@@ -88,10 +105,15 @@ module API
end
def load_projects
- ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ params = project_finder_params
+ verify_project_filters!(params)
+
+ ProjectsFinder.new(current_user: current_user, params: params).execute
end
def present_projects(projects, options = {})
+ verify_statistics_order_by_projects!
+
projects = reorder_projects(projects)
projects = apply_filters(projects)
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 38b636e4e5a..778925dd1df 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -60,12 +60,20 @@ module Gitlab
end
end
- def diff_file_with_old_path(old_path)
- diff_files.find { |diff_file| diff_file.old_path == old_path }
+ def diff_file_with_old_path(old_path, a_mode = nil)
+ if Feature.enabled?(:file_identifier_hash) && a_mode.present?
+ diff_files.find { |diff_file| diff_file.old_path == old_path && diff_file.a_mode == a_mode }
+ else
+ diff_files.find { |diff_file| diff_file.old_path == old_path }
+ end
end
- def diff_file_with_new_path(new_path)
- diff_files.find { |diff_file| diff_file.new_path == new_path }
+ def diff_file_with_new_path(new_path, b_mode = nil)
+ if Feature.enabled?(:file_identifier_hash) && b_mode.present?
+ diff_files.find { |diff_file| diff_file.new_path == new_path && diff_file.b_mode == b_mode }
+ else
+ diff_files.find { |diff_file| diff_file.new_path == new_path }
+ end
end
def clear_cache
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index a1c82ce9afc..1c21c35fa60 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -42,6 +42,10 @@ module Gitlab
@cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha)
end
+ def diff_file(position)
+ position.diff_file(project.repository)
+ end
+
private
def compare(start_sha, head_sha, straight: false)
diff --git a/lib/gitlab/diff/position_tracer/base_strategy.rb b/lib/gitlab/diff/position_tracer/base_strategy.rb
index 65049daabf4..61250bd2473 100644
--- a/lib/gitlab/diff/position_tracer/base_strategy.rb
+++ b/lib/gitlab/diff/position_tracer/base_strategy.rb
@@ -8,6 +8,7 @@ module Gitlab
delegate \
:project,
+ :diff_file,
:ac_diffs,
:bd_diffs,
:cd_diffs,
diff --git a/lib/gitlab/diff/position_tracer/image_strategy.rb b/lib/gitlab/diff/position_tracer/image_strategy.rb
index 79244a17951..046a6782dda 100644
--- a/lib/gitlab/diff/position_tracer/image_strategy.rb
+++ b/lib/gitlab/diff/position_tracer/image_strategy.rb
@@ -5,22 +5,26 @@ module Gitlab
class PositionTracer
class ImageStrategy < BaseStrategy
def trace(position)
+ a_path = position.old_path
b_path = position.new_path
+ diff_file = diff_file(position)
+ a_mode = diff_file&.a_mode
+ b_mode = diff_file&.b_mode
# If file exists in B->D (e.g. updated, renamed, removed), let the
# note become outdated.
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path, b_mode)
return { position: new_position(position, bd_diff), outdated: true } if bd_diff
# If file still exists in the new diff, update the position.
- cd_diff = cd_diffs.diff_file_with_new_path(bd_diff&.new_path || b_path)
+ cd_diff = cd_diffs.diff_file_with_new_path(b_path, b_mode)
return { position: new_position(position, cd_diff), outdated: false } if cd_diff
# If file exists in A->C (e.g. rebased and same changes were present
# in target branch), let the note become outdated.
- ac_diff = ac_diffs.diff_file_with_old_path(position.old_path)
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path, a_mode)
return { position: new_position(position, ac_diff), outdated: true } if ac_diff
diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb
index 8db0fc6f963..e3c1e549b96 100644
--- a/lib/gitlab/diff/position_tracer/line_strategy.rb
+++ b/lib/gitlab/diff/position_tracer/line_strategy.rb
@@ -76,16 +76,20 @@ module Gitlab
def trace_added_line(position)
b_path = position.new_path
b_line = position.new_line
+ diff_file = diff_file(position)
+ b_mode = diff_file&.b_mode
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path, b_mode)
d_path = bd_diff&.new_path || b_path
+ d_mode = bd_diff&.b_mode || b_mode
d_line = LineMapper.new(bd_diff).old_to_new(b_line)
if d_line
- cd_diff = cd_diffs.diff_file_with_new_path(d_path)
+ cd_diff = cd_diffs.diff_file_with_new_path(d_path, d_mode)
c_path = cd_diff&.old_path || d_path
+ c_mode = cd_diff&.a_mode || d_mode
c_line = LineMapper.new(cd_diff).new_to_old(d_line)
if c_line
@@ -98,7 +102,7 @@ module Gitlab
else
# If the line is no longer in the MR, we unfortunately cannot show
# the current state on the CD diff, so we treat it as outdated.
- ac_diff = ac_diffs.diff_file_with_new_path(c_path)
+ ac_diff = ac_diffs.diff_file_with_new_path(c_path, c_mode)
{ position: new_position(ac_diff, nil, c_line), outdated: true }
end
@@ -115,22 +119,26 @@ module Gitlab
def trace_removed_line(position)
a_path = position.old_path
a_line = position.old_line
+ diff_file = diff_file(position)
+ a_mode = diff_file&.a_mode
- ac_diff = ac_diffs.diff_file_with_old_path(a_path)
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path, a_mode)
c_path = ac_diff&.new_path || a_path
+ c_mode = ac_diff&.b_mode || a_mode
c_line = LineMapper.new(ac_diff).old_to_new(a_line)
if c_line
- cd_diff = cd_diffs.diff_file_with_old_path(c_path)
+ cd_diff = cd_diffs.diff_file_with_old_path(c_path, c_mode)
d_path = cd_diff&.new_path || c_path
+ d_mode = cd_diff&.b_mode || c_mode
d_line = LineMapper.new(cd_diff).old_to_new(c_line)
if d_line
# If the line is still in C but also in D, it has turned from a
# removed line into an unchanged one.
- bd_diff = bd_diffs.diff_file_with_new_path(d_path)
+ bd_diff = bd_diffs.diff_file_with_new_path(d_path, d_mode)
{ position: new_position(bd_diff, nil, d_line), outdated: true }
else
@@ -148,17 +156,21 @@ module Gitlab
a_line = position.old_line
b_path = position.new_path
b_line = position.new_line
+ diff_file = diff_file(position)
+ a_mode = diff_file&.a_mode
+ b_mode = diff_file&.b_mode
- ac_diff = ac_diffs.diff_file_with_old_path(a_path)
+ ac_diff = ac_diffs.diff_file_with_old_path(a_path, a_mode)
c_path = ac_diff&.new_path || a_path
+ c_mode = ac_diff&.b_mode || a_mode
c_line = LineMapper.new(ac_diff).old_to_new(a_line)
- bd_diff = bd_diffs.diff_file_with_old_path(b_path)
+ bd_diff = bd_diffs.diff_file_with_old_path(b_path, b_mode)
d_line = LineMapper.new(bd_diff).old_to_new(b_line)
- cd_diff = cd_diffs.diff_file_with_old_path(c_path)
+ cd_diff = cd_diffs.diff_file_with_old_path(c_path, c_mode)
if c_line && d_line
# If the line is still in C and D, it is still unchanged.
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 8c704c2ceea..1ca946b83c6 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -15,28 +15,9 @@ module Gitlab
oid: oid,
limit: limit
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout)
-
- data = []
- blob = nil
- response.each do |msg|
- if blob.nil?
- blob = msg
- end
-
- data << msg.data
+ GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout) do |response|
+ consume_blob_response(response)
end
-
- return if blob.oid.blank?
-
- data = data.join
-
- Gitlab::Git::Blob.new(
- id: blob.oid,
- size: blob.size,
- data: data,
- binary: Gitlab::Git::Blob.binary?(data)
- )
end
def batch_lfs_pointers(blob_ids)
@@ -47,9 +28,9 @@ module Gitlab
blob_ids: blob_ids
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
-
- map_lfs_pointers(response)
+ GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request, timeout: GitalyClient.medium_timeout) do |response|
+ map_lfs_pointers(response)
+ end
end
def get_blobs(revision_paths, limit = -1)
@@ -65,15 +46,15 @@ module Gitlab
limit: limit
)
- response = GitalyClient.call(
+ GitalyClient.streaming_call(
@gitaly_repo.storage_name,
:blob_service,
:get_blobs,
request,
timeout: GitalyClient.fast_timeout
- )
-
- GitalyClient::BlobsStitcher.new(response)
+ ) do |response|
+ GitalyClient::BlobsStitcher.new(response)
+ end
end
def get_blob_types(revision_paths, limit = -1)
@@ -89,15 +70,15 @@ module Gitlab
limit: limit
)
- response = GitalyClient.call(
+ GitalyClient.streaming_call(
@gitaly_repo.storage_name,
:blob_service,
:get_blobs,
request,
timeout: GitalyClient.fast_timeout
- )
-
- map_blob_types(response)
+ ) do |response|
+ map_blob_types(response)
+ end
end
def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
@@ -120,15 +101,15 @@ module Gitlab
GitalyClient.medium_timeout
end
- response = GitalyClient.call(
+ GitalyClient.streaming_call(
@gitaly_repo.storage_name,
:blob_service,
:get_new_lfs_pointers,
request,
timeout: timeout
- )
-
- map_lfs_pointers(response)
+ ) do |response|
+ map_lfs_pointers(response)
+ end
end
def get_all_lfs_pointers
@@ -136,13 +117,36 @@ module Gitlab
repository: @gitaly_repo
)
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
-
- map_lfs_pointers(response)
+ GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout) do |response|
+ map_lfs_pointers(response)
+ end
end
private
+ def consume_blob_response(response)
+ data = []
+ blob = nil
+ response.each do |msg|
+ if blob.nil?
+ blob = msg
+ end
+
+ data << msg.data
+ end
+
+ return if blob.oid.blank?
+
+ data = data.join
+
+ Gitlab::Git::Blob.new(
+ id: blob.oid,
+ size: blob.size,
+ data: data,
+ binary: Gitlab::Git::Blob.binary?(data)
+ )
+ end
+
def map_lfs_pointers(response)
response.flat_map do |message|
message.lfs_pointers.map do |lfs_pointer|
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index 264ea0488e7..07ddd315bcc 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -20,20 +20,20 @@ module Gitlab
when DashboardProcessingError
error(error.message, :unprocessable_entity)
when NOT_FOUND_ERROR
- error("#{dashboard_path} could not be found.", :not_found)
+ error(_("%{dashboard_path} could not be found.") % { dashboard_path: dashboard_path }, :not_found)
when PanelNotFoundError
error(error.message, :not_found)
when ::Grafana::Client::Error
error(error.message, :service_unavailable)
when MissingIntegrationError
- error('Proxy support for this API is not available currently', :bad_request)
+ error(_('Proxy support for this API is not available currently'), :bad_request)
else
raise error
end
end
def panels_not_found!(opts)
- raise PanelNotFoundError.new("No panels matching properties #{opts}")
+ raise PanelNotFoundError.new(_("No panels matching properties %{opts}") % { opts: opts })
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb b/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb
index fea0a24107e..c48a7ff25a5 100644
--- a/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/metric_endpoint_inserter.rb
@@ -6,7 +6,7 @@ module Gitlab
module Stages
class MetricEndpointInserter < BaseStage
def transform!
- raise Errors::DashboardProcessingError.new('Environment is required for Stages::MetricEndpointInserter') unless params[:environment]
+ raise Errors::DashboardProcessingError.new(_('Environment is required for Stages::MetricEndpointInserter')) unless params[:environment]
for_metrics do |metric|
metric[:prometheus_endpoint_path] = endpoint_for_metric(metric)
@@ -33,7 +33,11 @@ module Gitlab
end
def query_type(metric)
- metric[:query] ? :query : :query_range
+ if metric[:query]
+ ::Prometheus::ProxyService::PROMETHEUS_QUERY_API.to_sym
+ else
+ ::Prometheus::ProxyService::PROMETHEUS_QUERY_RANGE_API.to_sym
+ end
end
def query_for_metric(metric)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d13207d6c41..60918d6945b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -326,6 +326,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr ""
+msgid "%{dashboard_path} could not be found."
+msgstr ""
+
msgid "%{days} days until tags are automatically removed"
msgstr ""
@@ -4669,9 +4672,15 @@ msgstr ""
msgid "Cluster does not exist"
msgstr ""
+msgid "Cluster is required for Stages::ClusterEndpointInserter"
+msgstr ""
+
msgid "Cluster level"
msgstr ""
+msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter"
+msgstr ""
+
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr ""
@@ -7070,6 +7079,9 @@ msgstr ""
msgid "Dashboard"
msgstr ""
+msgid "Dashboard uid not found"
+msgstr ""
+
msgid "DashboardProjects|All"
msgstr ""
@@ -7094,6 +7106,9 @@ msgstr ""
msgid "Data is still calculating..."
msgstr ""
+msgid "Datasource name not found"
+msgstr ""
+
msgid "Date"
msgstr ""
@@ -8555,6 +8570,9 @@ msgstr ""
msgid "Environment does not have deployments"
msgstr ""
+msgid "Environment is required for Stages::MetricEndpointInserter"
+msgstr ""
+
msgid "Environment is required for Stages::VariableEndpointInserter"
msgstr ""
@@ -10967,6 +10985,9 @@ msgstr ""
msgid "Grafana URL"
msgstr ""
+msgid "Grafana response contains invalid json"
+msgstr ""
+
msgid "GrafanaIntegration|API Token"
msgstr ""
@@ -11084,6 +11105,9 @@ msgstr ""
msgid "Group info:"
msgstr ""
+msgid "Group is required when cluster_type is :group"
+msgstr ""
+
msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
@@ -15068,6 +15092,9 @@ msgstr ""
msgid "No other labels with such name or description"
msgstr ""
+msgid "No panels matching properties %{opts}"
+msgstr ""
+
msgid "No parent group"
msgstr ""
@@ -15994,6 +16021,9 @@ msgstr ""
msgid "Parent epic is not present."
msgstr ""
+msgid "Parsing error for param :embed_json. %{message}"
+msgstr ""
+
msgid "Part of merge request changes"
msgstr ""
@@ -17299,6 +17329,9 @@ msgstr ""
msgid "Project has too many %{label_for_message} to search"
msgstr ""
+msgid "Project is required when cluster_type is :project"
+msgstr ""
+
msgid "Project members"
msgstr ""
@@ -17416,9 +17449,6 @@ msgstr ""
msgid "ProjectService|Comment"
msgstr ""
-msgid "ProjectService|Comment will be posted on each event"
-msgstr ""
-
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr ""
@@ -18211,6 +18241,9 @@ msgstr ""
msgid "Provider"
msgstr ""
+msgid "Proxy support for this API is not available currently"
+msgstr ""
+
msgid "Pseudonymizer data collection"
msgstr ""
@@ -24212,6 +24245,9 @@ msgstr ""
msgid "Unreachable"
msgstr ""
+msgid "Unrecognized cluster type"
+msgstr ""
+
msgid "Unresolve"
msgstr ""
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 929927ec1c4..bd71a8186ad 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -262,6 +262,17 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to match_array([public_project]) }
end
+ describe 'filter by repository_storage' do
+ let(:params) { { repository_storage: 'nfs-05' } }
+ let!(:project) { create(:project, :public) }
+
+ before do
+ project.update_columns(repository_storage: 'nfs-05')
+ end
+
+ it { is_expected.to match_array([project]) }
+ end
+
describe 'sorting' do
let(:params) { { sort: 'name_asc' } }
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index e5710641f81..d6d8733b290 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -147,6 +147,20 @@ describe('DynamicField', () => {
.text(),
).toBe(defaultProps.help);
});
+
+ it('renders description with help text as HTML', () => {
+ const helpHTML = 'The URL of the project';
+
+ createComponent({
+ help: helpHTML,
+ });
+
+ expect(
+ findGlFormGroup()
+ .find('small')
+ .html(),
+ ).toContain(helpHTML);
+ });
});
describe('label text', () => {
diff --git a/spec/helpers/services_helper_spec.rb b/spec/helpers/services_helper_spec.rb
index edc14f86a50..cb4e016d9d1 100644
--- a/spec/helpers/services_helper_spec.rb
+++ b/spec/helpers/services_helper_spec.rb
@@ -7,9 +7,4 @@ describe ServicesHelper do
it { expect(event_action_title('comment')).to eq 'Comment' }
it { expect(event_action_title('something')).to eq 'Something' }
end
-
- describe 'event_action_description' do
- it { expect(event_action_description('comment')).to eq 'Comment will be posted on each event' }
- it { expect(event_action_description('something')).to eq nil }
- end
end
diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
index 900816af53a..e33d7d80361 100644
--- a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb
@@ -234,5 +234,118 @@ describe Gitlab::Diff::PositionTracer::ImageStrategy do
end
end
end
+
+ describe 'symlink scenarios' do
+ let(:new_file) { old_file_status == :new }
+ let(:deleted_file) { old_file_status == :deleted }
+ let(:renamed_file) { old_file_status == :renamed }
+
+ let(:file_identifier) { "#{file_name}-#{new_file}-#{deleted_file}-#{renamed_file}" }
+ let(:file_identifier_hash) { Digest::SHA1.hexdigest(file_identifier) }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image', file_identifier_hash: file_identifier_hash) }
+
+ let(:update_file_commit) do
+ initial_commit
+
+ update_file(
+ branch_name,
+ file_name,
+ Base64.encode64('morecontent')
+ )
+ end
+
+ let(:delete_file_commit) do
+ initial_commit
+
+ delete_file(branch_name, file_name)
+ end
+
+ let(:create_second_file_commit) do
+ initial_commit
+
+ create_file(
+ branch_name,
+ second_file_name,
+ Base64.encode64('morecontent')
+ )
+ end
+
+ before do
+ stub_feature_flags(file_identifier_hash: true)
+ end
+
+ describe 'from symlink to image' do
+ let(:initial_commit) { project.commit('a19c7f9a147e35e535c797cf148d29c24dac5544') }
+ let(:symlink_to_image_commit) { project.commit('8cfca8420812e5bd7479aa32cf33e0c95a3ca576') }
+ let(:branch_name) { 'diff-files-symlink-to-image' }
+ let(:file_name) { 'symlink-to-image.png' }
+
+ context "when the old position is on the new image file" do
+ let(:old_file_status) { :new }
+
+ context "when the image file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
+
+ it "returns the new position" do
+ expect_new_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context "when the image file's content was changed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) }
+ let(:change_diff_refs) { diff_refs(symlink_to_image_commit, update_file_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+
+ context "when the image file was removed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_image_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(symlink_to_image_commit, delete_file_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+ end
+ end
+
+ describe 'from image to symlink' do
+ let(:initial_commit) { project.commit('d10dcdfbbb2b59a959a5f5d66a4adf28f0ea4008') }
+ let(:image_to_symlink_commit) { project.commit('3e94fdaa60da8aed38401b91bc56be70d54ca424') }
+ let(:branch_name) { 'diff-files-image-to-symlink' }
+ let(:file_name) { 'image-to-symlink.png' }
+
+ context "when the old position is on the added image file" do
+ let(:old_file_status) { :new }
+
+ context "when the image file gets changed to a symlink between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit.parent, initial_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit.parent, image_to_symlink_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, image_to_symlink_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name
+ )
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
index 7f4902c5b86..46fb6c566b4 100644
--- a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
@@ -1801,5 +1801,143 @@ describe Gitlab::Diff::PositionTracer::LineStrategy do
end
end
end
+
+ describe 'symlink scenarios' do
+ let(:new_file) { old_file_status == :new }
+ let(:deleted_file) { old_file_status == :deleted }
+ let(:renamed_file) { old_file_status == :renamed }
+
+ let(:file_identifier) { "#{file_name}-#{new_file}-#{deleted_file}-#{renamed_file}" }
+ let(:file_identifier_hash) { Digest::SHA1.hexdigest(file_identifier) }
+
+ let(:update_line_commit) do
+ update_file(
+ branch_name,
+ file_name,
+ <<-CONTENT.strip_heredoc
+ A
+ BB
+ C
+ CONTENT
+ )
+ end
+
+ let(:delete_file_commit) do
+ delete_file(branch_name, file_name)
+ end
+
+ let(:create_second_file_commit) do
+ create_file(
+ branch_name,
+ second_file_name,
+ <<-CONTENT.strip_heredoc
+ D
+ E
+ CONTENT
+ )
+ end
+
+ before do
+ stub_feature_flags(file_identifier_hash: true)
+ end
+
+ describe 'from symlink to text' do
+ let(:initial_commit) { project.commit('0e5b363105e9176a77bac94d7ff6d8c4fb35c3eb') }
+ let(:symlink_to_text_commit) { project.commit('689815e617abc6889f1fded4834d2dd7d942a58e') }
+ let(:branch_name) { 'diff-files-symlink-to-text' }
+ let(:file_name) { 'symlink-to-text.txt' }
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 3, file_identifier_hash: file_identifier_hash) }
+
+ before do
+ create_branch('diff-files-symlink-to-text-test', branch_name)
+ end
+
+ context "when the old position is on the new text file" do
+ let(:old_file_status) { :new }
+
+ context "when the text file's content was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) }
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the text file's content has change, but the line was unchanged between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+
+ it "returns the new position" do
+ expect_new_position(
+ new_path: old_position.new_path,
+ new_line: old_position.new_line
+ )
+ end
+ end
+
+ context "when the text file's line was changed between the old and the new diff" do
+ let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2, file_identifier_hash: file_identifier_hash) }
+
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) }
+ let(:change_diff_refs) { diff_refs(symlink_to_text_commit, update_line_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 2,
+ new_line: nil
+ )
+ end
+ end
+
+ context "when the text file was removed between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit, symlink_to_text_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) }
+ let(:change_diff_refs) { diff_refs(symlink_to_text_commit, delete_file_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+
+ describe 'from text to symlink' do
+ let(:initial_commit) { project.commit('3db7bd90bab8ce8f02c9818590b84739a2e97230') }
+ let(:text_to_symlink_commit) { project.commit('5e2c2708c2e403dece5dd25759369150aac51644') }
+ let(:branch_name) { 'diff-files-text-to-symlink' }
+ let(:file_name) { 'text-to-symlink.txt' }
+
+ context "when the position is on the added text file" do
+ let(:old_file_status) { :new }
+
+ context "when the text file gets changed to a symlink between the old and the new diff" do
+ let(:old_diff_refs) { diff_refs(initial_commit.parent, initial_commit) }
+ let(:new_diff_refs) { diff_refs(initial_commit.parent, text_to_symlink_commit) }
+ let(:change_diff_refs) { diff_refs(initial_commit, text_to_symlink_commit) }
+
+ it "returns the position of the change" do
+ expect_change_position(
+ old_path: file_name,
+ new_path: file_name,
+ old_line: 3,
+ new_line: nil
+ )
+ end
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 385261e0ee9..439648aba5a 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -47,8 +47,8 @@ describe Ci::Bridge do
CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA
CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG
CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
- CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PIPELINE_IID
- CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
+ CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE
+ CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED
]
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a646f08ffb4..e3d6ae82995 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2380,6 +2380,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_ROOT_NAMESPACE', value: project.namespace.root_ancestor.path, public: true, masked: false },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true, masked: false },
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
{ key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false },
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index e8ba4eed4ec..a19ebecac6d 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -130,6 +130,24 @@ describe GlobalPolicy do
end
end
+ describe 'using project statistics filters' do
+ context 'regular user' do
+ it { is_expected.not_to be_allowed(:use_project_statistics_filters) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:user, :admin) }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:use_project_statistics_filters) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:use_project_statistics_filters) }
+ end
+ end
+ end
+
shared_examples 'access allowed when terms accepted' do |ability|
it { is_expected.not_to be_allowed(ability) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index bdbaa178caa..6f0fe181991 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -584,6 +584,85 @@ describe API::Projects do
end
end
+ context 'sorting by project statistics' do
+ %w(repository_size storage_size wiki_size).each do |order_by|
+ context "sorting by #{order_by}" do
+ before do
+ ProjectStatistics.update_all(order_by => 100)
+ project4.statistics.update_columns(order_by => 10)
+ project.statistics.update_columns(order_by => 200)
+ end
+
+ context 'admin user' do
+ let(:current_user) { admin }
+
+ context "when sorting by #{order_by} ascendingly" do
+ it 'returns a properly sorted list of projects' do
+ get api('/projects', current_user), params: { order_by: order_by, sort: :asc }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(project4.id)
+ end
+ end
+
+ context "when sorting by #{order_by} descendingly" do
+ it 'returns a properly sorted list of projects' do
+ get api('/projects', current_user), params: { order_by: order_by, sort: :desc }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(project.id)
+ end
+ end
+ end
+
+ context 'non-admin user' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+
+ it 'returns projects ordered normally' do
+ get api('/projects', current_user), params: { order_by: order_by }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to eq(projects.map(&:id).reverse)
+ end
+ end
+ end
+ end
+ end
+
+ context 'filtering by repository_storage' do
+ before do
+ [project, project3].each { |proj| proj.update_columns(repository_storage: 'nfs-11') }
+ # Since we don't actually have Gitaly configured with an nfs-11 storage, an error would be raised
+ # when we present the projects in a response, as we ask Gitaly for stuff like default branch and Gitaly
+ # is not configured for a nfs-11 storage. So we trick Rails into thinking the storage for these projects
+ # is still default (in reality, it is).
+ allow_any_instance_of(Project).to receive(:repository_storage).and_return('default')
+ end
+
+ context 'admin user' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { repository_storage: 'nfs-11' } }
+ let(:current_user) { admin }
+ let(:projects) { [project, project3] }
+ end
+ end
+
+ context 'non-admin user' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { repository_storage: 'nfs-11' } }
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
+ end
+
context 'with keyset pagination' do
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3] }
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 130650b7e2e..db70c3e8248 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -29,6 +29,10 @@ module TestEnv
'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455',
'symlink-expand-diff' => '81e6355',
+ 'diff-files-symlink-to-image' => '8cfca84',
+ 'diff-files-image-to-symlink' => '3e94fda',
+ 'diff-files-symlink-to-text' => '689815e',
+ 'diff-files-text-to-symlink' => '5e2c270',
'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d',
'pages-deploy' => '7897d5b',