Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-19 21:08:32 +00:00
parent ce493944f4
commit 60082b335c
50 changed files with 708 additions and 124 deletions

View File

@ -135,8 +135,11 @@ export default {
:label-for="fieldId" :label-for="fieldId"
:invalid-feedback="__('This field is required.')" :invalid-feedback="__('This field is required.')"
:state="valid" :state="valid"
:description="help"
> >
<template #description>
<span v-html="help"></span>
</template>
<template v-if="isCheckbox"> <template v-if="isCheckbox">
<input :name="fieldName" type="hidden" value="false" /> <input :name="fieldName" type="hidden" value="false" />
<gl-form-checkbox v-model="model" v-bind="sharedProps"> <gl-form-checkbox v-model="model" v-bind="sharedProps">

View File

@ -54,9 +54,8 @@ module IntegrationsActions
end end
def integration def integration
# Using instance variable `@service` still required as it's used in ServiceParams # Using instance variable `@service` still required as it's used in ServiceParams.
# and app/views/shared/_service_settings.html.haml. Should be removed once # Should be removed once that is refactored to use `@integration`.
# those 2 are refactored to use `@integration`.
@integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables @integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end

View File

@ -23,6 +23,7 @@
# min_access_level: integer # min_access_level: integer
# last_activity_after: datetime # last_activity_after: datetime
# last_activity_before: datetime # last_activity_before: datetime
# repository_storage: string
# #
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
include CustomAttributesFilter include CustomAttributesFilter
@ -75,6 +76,7 @@ class ProjectsFinder < UnionFinder
collection = by_deleted_status(collection) collection = by_deleted_status(collection)
collection = by_last_activity_after(collection) collection = by_last_activity_after(collection)
collection = by_last_activity_before(collection) collection = by_last_activity_before(collection)
collection = by_repository_storage(collection)
collection collection
end end
@ -197,6 +199,14 @@ class ProjectsFinder < UnionFinder
end end
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) def sort(items)
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc
end end

View File

@ -44,13 +44,6 @@ module ServicesHelper
end end
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 def service_save_button
button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') + icon('spinner spin', class: 'hidden js-btn-spinner') +
@ -90,7 +83,7 @@ module ServicesHelper
def scoped_test_integration_path(integration) def scoped_test_integration_path(integration)
if @project.present? if @project.present?
test_project_settings_integration_path(@project, integration) test_project_service_path(@project, integration)
elsif @group.present? elsif @group.present?
test_group_settings_integration_path(@group, integration) test_group_settings_integration_path(@group, integration)
else else
@ -102,22 +95,36 @@ module ServicesHelper
Feature.enabled?(:integration_form_refactor, @project) Feature.enabled?(:integration_form_refactor, @project)
end end
def trigger_events_for_service def integration_form_data(integration)
return [] unless integration_form_refactor? {
show_active: integration.show_active_box?.to_s,
ServiceEventSerializer.new(service: @service).represent(@service.configurable_events).to_json 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 end
def fields_for_service def trigger_events_for_service(integration)
return [] unless integration_form_refactor? 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 end
def show_service_trigger_events? def fields_for_service(integration)
return false if @service.is_a?(JiraService) || integration_form_refactor? 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 end
extend self extend self

View File

@ -1925,6 +1925,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_PATH', value: full_path) .append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug) .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) .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_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level)) .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) .append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)

View File

@ -105,6 +105,9 @@ class GlobalPolicy < BasePolicy
enable :update_custom_attribute enable :update_custom_attribute
end 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 rule { external_user }.prevent :create_snippet
end end

View File

@ -80,7 +80,7 @@ module Metrics
def fetch_dashboard def fetch_dashboard
uid = GrafanaUidParser.new(grafana_url, project).parse 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) response = client.get_dashboard(uid: uid)
@ -89,7 +89,7 @@ module Metrics
def fetch_datasource(dashboard) def fetch_datasource(dashboard)
name = DatasourceNameParser.new(grafana_url, dashboard).parse 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) response = client.get_datasource(name: name)
@ -115,7 +115,7 @@ module Metrics
def parse_json(json) def parse_json(json)
Gitlab::Json.parse(json, symbolize_names: true) Gitlab::Json.parse(json, symbolize_names: true)
rescue JSON::ParserError rescue JSON::ParserError
raise DashboardProcessingError.new('Grafana response contains invalid json') raise DashboardProcessingError.new(_('Grafana response contains invalid json'))
end end
end end

View File

@ -39,7 +39,7 @@ module Metrics
end end
def invalid_embed_json!(message) 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 end
end end

View File

@ -4,7 +4,7 @@
%p #{@service.description} template. %p #{@service.description} template.
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form| = 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 .footer-block.row-content-block
= form.submit 'Save', class: 'btn btn-success' = form.submit 'Save', class: 'btn btn-success'

View File

@ -1,5 +1,6 @@
- add_to_breadcrumbs "Service Templates", admin_application_settings_services_path - add_to_breadcrumbs "Service Templates", admin_application_settings_services_path
- breadcrumb_title @service.title - breadcrumb_title @service.title
- page_title @service.title, "Service Templates" - page_title @service.title, "Service Templates"
- @content_class = 'limit-container-width' unless fluid_layout
= render 'form' = render 'form'

View File

@ -216,7 +216,7 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Appearance') = _('Appearance')
= nav_link(controller: :application_settings) do = nav_link(controller: [:application_settings, :integrations]) do
= link_to general_admin_application_settings_path do = link_to general_admin_application_settings_path do
.nav-icon-container .nav-icon-container
= sprite_icon('settings') = sprite_icon('settings')
@ -224,7 +224,7 @@
= _('Settings') = _('Settings')
%ul.sidebar-sub-level-items.qa-admin-sidebar-settings-submenu %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 = link_to general_admin_application_settings_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Settings') = _('Settings')
@ -233,7 +233,7 @@
= link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do = link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
%span %span
= _('General') = _('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 = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do
%span %span
= _('Integrations') = _('Integrations')

View File

@ -11,7 +11,7 @@
%p= @service.detailed_description %p= @service.detailed_description
.col-lg-8 .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| = 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 .footer-block.row-content-block
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer } %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button = service_save_button

View File

@ -1,22 +1,21 @@
= form_errors(@service) = form_errors(integration)
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) - if lookup_context.template_exists?('help', "projects/services/#{integration.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: @service = render "projects/services/#{integration.to_param}/help", subject: integration
- elsif @service.help.present? - elsif integration.help.present?
.info-well .info-well
.well-segment .well-segment
= markdown @service.help = markdown integration.help
.service-settings .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, .js-vue-integration-settings{ data: integration_form_data(integration) }
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 } }
- if show_service_trigger_events? - if show_service_trigger_events?(integration)
.form-group.row .form-group.row
%label.col-form-label.col-sm-2= _('Trigger') %label.col-form-label.col-sm-2= _('Trigger')
.col-sm-10 .col-sm-10
- @service.configurable_events.each do |event| - integration.configurable_events.each do |event|
.form-group .form-group
.form-check .form-check
= form.check_box service_event_field_name(event), class: 'form-check-input' = 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 %strong
= event.humanize = event.humanize
- field = @service.event_field(event) - field = integration.event_field(event)
- if field - if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.text-muted %p.text-muted
= @service.class.event_description(event) = integration.class.event_description(event)
- unless integration_form_refactor? - unless integration_form_refactor?
- @service.global_fields.each do |field| - integration.global_fields.each do |field|
= render 'shared/field', form: form, field: field = render 'shared/field', form: form, field: field

View File

@ -1,5 +1,6 @@
- add_to_breadcrumbs _('Integrations'), scoped_integrations_path - add_to_breadcrumbs _('Integrations'), scoped_integrations_path
- breadcrumb_title @integration.title - breadcrumb_title @integration.title
- page_title @integration.title, _('Integrations') - page_title @integration.title, _('Integrations')
- @content_class = 'limit-container-width' unless fluid_layout
= render 'shared/integrations/form', integration: @integration = render 'shared/integrations/form', integration: @integration

View File

@ -0,0 +1,5 @@
---
title: Refine UI of integration form
merge_request: 34707
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add CI_PROJECT_ROOT_NAMESPACE predefined environment variable
merge_request: 34733
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Allow advanced API projects filtering for admins
merge_request: 32879
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix Gitaly duration timings of BlobService RPCs
merge_request: 34906
author:
type: other

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 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 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); 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 20200604174558
20200605003204 20200605003204
20200605093113 20200605093113
20200605160806
20200605160836
20200605160851
20200608072931 20200608072931
20200608075553 20200608075553
20200608214008 20200608214008

View File

@ -103,10 +103,10 @@ Some feature flags can be enabled or disabled on a per project basis:
Feature.enable(:<feature flag>, Project.find(<project id>)) Feature.enable(:<feature flag>, Project.find(<project id>))
``` ```
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 ```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 When the feature is ready, GitLab will remove the feature flag, the option for

View File

@ -142,7 +142,7 @@ Example of response
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202525) in GitLab 13.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202525) in GitLab 13.0.
CAUTION: **Caution:** 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 ```plaintext
GET /projects/:id/pipelines/:pipeline_id/test_report GET /projects/:id/pipelines/:pipeline_id/test_report

View File

@ -45,7 +45,7 @@ GET /projects
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `archived` | boolean | no | Limit by archived status | | `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | | `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` | | `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` | string | no | Return list of projects matching the search criteria |
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` | | `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 | | `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_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 | | `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:** NOTE: **Note:**
This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options. This endpoint supports [keyset pagination](README.md#keyset-based-pagination) for selected `order_by` options.

View File

@ -206,7 +206,7 @@ cunit:
junit: ./my-cunit-test.xml junit: ./my-cunit-test.xml
``` ```
### .Net example ### .NET example
The [JunitXML.TestLogger](https://www.nuget.org/packages/JunitXml.TestLogger/) NuGet 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 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 ## 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 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 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). 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 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. feature is disabled due to some performance issues with very large data sets.
@ -266,7 +268,9 @@ Feature.enable(:junit_pipeline_view, Project.find(<your-project-id-here>))
## Viewing JUnit screenshots on GitLab ## 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. 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. 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. This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.

View File

@ -341,6 +341,7 @@ export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-foss"
export CI_PROJECT_NAME="gitlab-foss" export CI_PROJECT_NAME="gitlab-foss"
export CI_PROJECT_TITLE="GitLab FOSS" export CI_PROJECT_TITLE="GitLab FOSS"
export CI_PROJECT_NAMESPACE="gitlab-org" export CI_PROJECT_NAMESPACE="gitlab-org"
export CI_PROJECT_ROOT_NAMESPACE="gitlab-org"
export CI_PROJECT_PATH="gitlab-org/gitlab-foss" export CI_PROJECT_PATH="gitlab-org/gitlab-foss"
export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-foss" export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-foss"
export CI_REGISTRY="registry.example.com" 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 ++ CI_PROJECT_PATH_SLUG=gitlab-examples-ci-debug-trace
++ export CI_PROJECT_NAMESPACE=gitlab-examples ++ export CI_PROJECT_NAMESPACE=gitlab-examples
++ 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 ++ export CI_PROJECT_URL=https://gitlab.com/gitlab-examples/ci-debug-trace
++ 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 ++ export CI_PROJECT_VISIBILITY=public

View File

@ -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_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_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_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` | 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_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`) | | `CI_PROJECT_REPOSITORY_LANGUAGES` | 12.3 | all | Comma-separated, lowercased list of the languages used in the repository (e.g. `ruby,javascript,html,css`) |

View File

@ -543,6 +543,7 @@ module API
finder_params[:id_before] = params[:id_before] if params[:id_before] 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_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[: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 finder_params
end end

View File

@ -6,6 +6,8 @@ module API
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend Grape::API::Helpers extend Grape::API::Helpers
STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze
params :optional_project_params_ce do params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project' 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`' optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`'

View File

@ -17,6 +17,7 @@ module API
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled] 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_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics] 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] lang = params[:with_programming_language]
projects = projects.with_programming_language(lang) if lang 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) attrs.delete(:repository_storage) unless can?(current_user, :change_repository_storage, project)
end 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) def delete_project(user_project)
destroy_conditionally!(user_project) do destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
@ -52,8 +67,9 @@ module API
end end
params :sort_params do params :sort_params do
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], optional :order_by, type: String,
default: 'created_at', desc: 'Return projects ordered by field' 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', optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order' desc: 'Return projects sorted in ascending and descending order'
end 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 :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_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 :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 use :optional_filter_params_ee
end end
@ -88,10 +105,15 @@ module API
end end
def load_projects 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 end
def present_projects(projects, options = {}) def present_projects(projects, options = {})
verify_statistics_order_by_projects!
projects = reorder_projects(projects) projects = reorder_projects(projects)
projects = apply_filters(projects) projects = apply_filters(projects)

View File

@ -60,12 +60,20 @@ module Gitlab
end end
end end
def diff_file_with_old_path(old_path) def diff_file_with_old_path(old_path, a_mode = nil)
diff_files.find { |diff_file| diff_file.old_path == old_path } 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 end
def diff_file_with_new_path(new_path) def diff_file_with_new_path(new_path, b_mode = nil)
diff_files.find { |diff_file| diff_file.new_path == new_path } 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 end
def clear_cache def clear_cache

View File

@ -42,6 +42,10 @@ module Gitlab
@cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha) @cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha)
end end
def diff_file(position)
position.diff_file(project.repository)
end
private private
def compare(start_sha, head_sha, straight: false) def compare(start_sha, head_sha, straight: false)

View File

@ -8,6 +8,7 @@ module Gitlab
delegate \ delegate \
:project, :project,
:diff_file,
:ac_diffs, :ac_diffs,
:bd_diffs, :bd_diffs,
:cd_diffs, :cd_diffs,

View File

@ -5,22 +5,26 @@ module Gitlab
class PositionTracer class PositionTracer
class ImageStrategy < BaseStrategy class ImageStrategy < BaseStrategy
def trace(position) def trace(position)
a_path = position.old_path
b_path = position.new_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 # If file exists in B->D (e.g. updated, renamed, removed), let the
# note become outdated. # 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 return { position: new_position(position, bd_diff), outdated: true } if bd_diff
# If file still exists in the new diff, update the position. # 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 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 # If file exists in A->C (e.g. rebased and same changes were present
# in target branch), let the note become outdated. # 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 return { position: new_position(position, ac_diff), outdated: true } if ac_diff

View File

@ -76,16 +76,20 @@ module Gitlab
def trace_added_line(position) def trace_added_line(position)
b_path = position.new_path b_path = position.new_path
b_line = position.new_line 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_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) d_line = LineMapper.new(bd_diff).old_to_new(b_line)
if d_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_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) c_line = LineMapper.new(cd_diff).new_to_old(d_line)
if c_line if c_line
@ -98,7 +102,7 @@ module Gitlab
else else
# If the line is no longer in the MR, we unfortunately cannot show # 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. # 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 } { position: new_position(ac_diff, nil, c_line), outdated: true }
end end
@ -115,22 +119,26 @@ module Gitlab
def trace_removed_line(position) def trace_removed_line(position)
a_path = position.old_path a_path = position.old_path
a_line = position.old_line 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_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) c_line = LineMapper.new(ac_diff).old_to_new(a_line)
if c_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_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) d_line = LineMapper.new(cd_diff).old_to_new(c_line)
if d_line if d_line
# If the line is still in C but also in D, it has turned from a # If the line is still in C but also in D, it has turned from a
# removed line into an unchanged one. # 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 } { position: new_position(bd_diff, nil, d_line), outdated: true }
else else
@ -148,17 +156,21 @@ module Gitlab
a_line = position.old_line a_line = position.old_line
b_path = position.new_path b_path = position.new_path
b_line = position.new_line 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_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) 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) 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 c_line && d_line
# If the line is still in C and D, it is still unchanged. # If the line is still in C and D, it is still unchanged.

View File

@ -15,28 +15,9 @@ module Gitlab
oid: oid, oid: oid,
limit: limit limit: limit
) )
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout) GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_blob, request, timeout: GitalyClient.fast_timeout) do |response|
consume_blob_response(response)
data = []
blob = nil
response.each do |msg|
if blob.nil?
blob = msg
end
data << msg.data
end 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 end
def batch_lfs_pointers(blob_ids) def batch_lfs_pointers(blob_ids)
@ -47,9 +28,9 @@ module Gitlab
blob_ids: blob_ids blob_ids: blob_ids
) )
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request, timeout: GitalyClient.medium_timeout) GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request, timeout: GitalyClient.medium_timeout) do |response|
map_lfs_pointers(response)
map_lfs_pointers(response) end
end end
def get_blobs(revision_paths, limit = -1) def get_blobs(revision_paths, limit = -1)
@ -65,15 +46,15 @@ module Gitlab
limit: limit limit: limit
) )
response = GitalyClient.call( GitalyClient.streaming_call(
@gitaly_repo.storage_name, @gitaly_repo.storage_name,
:blob_service, :blob_service,
:get_blobs, :get_blobs,
request, request,
timeout: GitalyClient.fast_timeout timeout: GitalyClient.fast_timeout
) ) do |response|
GitalyClient::BlobsStitcher.new(response)
GitalyClient::BlobsStitcher.new(response) end
end end
def get_blob_types(revision_paths, limit = -1) def get_blob_types(revision_paths, limit = -1)
@ -89,15 +70,15 @@ module Gitlab
limit: limit limit: limit
) )
response = GitalyClient.call( GitalyClient.streaming_call(
@gitaly_repo.storage_name, @gitaly_repo.storage_name,
:blob_service, :blob_service,
:get_blobs, :get_blobs,
request, request,
timeout: GitalyClient.fast_timeout timeout: GitalyClient.fast_timeout
) ) do |response|
map_blob_types(response)
map_blob_types(response) end
end end
def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil) def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
@ -120,15 +101,15 @@ module Gitlab
GitalyClient.medium_timeout GitalyClient.medium_timeout
end end
response = GitalyClient.call( GitalyClient.streaming_call(
@gitaly_repo.storage_name, @gitaly_repo.storage_name,
:blob_service, :blob_service,
:get_new_lfs_pointers, :get_new_lfs_pointers,
request, request,
timeout: timeout timeout: timeout
) ) do |response|
map_lfs_pointers(response)
map_lfs_pointers(response) end
end end
def get_all_lfs_pointers def get_all_lfs_pointers
@ -136,13 +117,36 @@ module Gitlab
repository: @gitaly_repo repository: @gitaly_repo
) )
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout) GitalyClient.streaming_call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request, timeout: GitalyClient.medium_timeout) do |response|
map_lfs_pointers(response)
map_lfs_pointers(response) end
end end
private 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) def map_lfs_pointers(response)
response.flat_map do |message| response.flat_map do |message|
message.lfs_pointers.map do |lfs_pointer| message.lfs_pointers.map do |lfs_pointer|

View File

@ -20,20 +20,20 @@ module Gitlab
when DashboardProcessingError when DashboardProcessingError
error(error.message, :unprocessable_entity) error(error.message, :unprocessable_entity)
when NOT_FOUND_ERROR 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 when PanelNotFoundError
error(error.message, :not_found) error(error.message, :not_found)
when ::Grafana::Client::Error when ::Grafana::Client::Error
error(error.message, :service_unavailable) error(error.message, :service_unavailable)
when MissingIntegrationError 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 else
raise error raise error
end end
end end
def panels_not_found!(opts) 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 end
end end

View File

@ -6,7 +6,7 @@ module Gitlab
module Stages module Stages
class MetricEndpointInserter < BaseStage class MetricEndpointInserter < BaseStage
def transform! 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| for_metrics do |metric|
metric[:prometheus_endpoint_path] = endpoint_for_metric(metric) metric[:prometheus_endpoint_path] = endpoint_for_metric(metric)
@ -33,7 +33,11 @@ module Gitlab
end end
def query_type(metric) 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 end
def query_for_metric(metric) def query_for_metric(metric)

View File

@ -326,6 +326,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}" msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr "" msgstr ""
msgid "%{dashboard_path} could not be found."
msgstr ""
msgid "%{days} days until tags are automatically removed" msgid "%{days} days until tags are automatically removed"
msgstr "" msgstr ""
@ -4669,9 +4672,15 @@ msgstr ""
msgid "Cluster does not exist" msgid "Cluster does not exist"
msgstr "" msgstr ""
msgid "Cluster is required for Stages::ClusterEndpointInserter"
msgstr ""
msgid "Cluster level" msgid "Cluster level"
msgstr "" msgstr ""
msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter"
msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr "" msgstr ""
@ -7070,6 +7079,9 @@ msgstr ""
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
msgid "Dashboard uid not found"
msgstr ""
msgid "DashboardProjects|All" msgid "DashboardProjects|All"
msgstr "" msgstr ""
@ -7094,6 +7106,9 @@ msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
msgstr "" msgstr ""
msgid "Datasource name not found"
msgstr ""
msgid "Date" msgid "Date"
msgstr "" msgstr ""
@ -8555,6 +8570,9 @@ msgstr ""
msgid "Environment does not have deployments" msgid "Environment does not have deployments"
msgstr "" msgstr ""
msgid "Environment is required for Stages::MetricEndpointInserter"
msgstr ""
msgid "Environment is required for Stages::VariableEndpointInserter" msgid "Environment is required for Stages::VariableEndpointInserter"
msgstr "" msgstr ""
@ -10967,6 +10985,9 @@ msgstr ""
msgid "Grafana URL" msgid "Grafana URL"
msgstr "" msgstr ""
msgid "Grafana response contains invalid json"
msgstr ""
msgid "GrafanaIntegration|API Token" msgid "GrafanaIntegration|API Token"
msgstr "" msgstr ""
@ -11084,6 +11105,9 @@ msgstr ""
msgid "Group info:" msgid "Group info:"
msgstr "" msgstr ""
msgid "Group is required when cluster_type is :group"
msgstr ""
msgid "Group maintainers can register group runners in the %{link}" msgid "Group maintainers can register group runners in the %{link}"
msgstr "" msgstr ""
@ -15068,6 +15092,9 @@ msgstr ""
msgid "No other labels with such name or description" msgid "No other labels with such name or description"
msgstr "" msgstr ""
msgid "No panels matching properties %{opts}"
msgstr ""
msgid "No parent group" msgid "No parent group"
msgstr "" msgstr ""
@ -15994,6 +16021,9 @@ msgstr ""
msgid "Parent epic is not present." msgid "Parent epic is not present."
msgstr "" msgstr ""
msgid "Parsing error for param :embed_json. %{message}"
msgstr ""
msgid "Part of merge request changes" msgid "Part of merge request changes"
msgstr "" msgstr ""
@ -17299,6 +17329,9 @@ msgstr ""
msgid "Project has too many %{label_for_message} to search" msgid "Project has too many %{label_for_message} to search"
msgstr "" msgstr ""
msgid "Project is required when cluster_type is :project"
msgstr ""
msgid "Project members" msgid "Project members"
msgstr "" msgstr ""
@ -17416,9 +17449,6 @@ msgstr ""
msgid "ProjectService|Comment" msgid "ProjectService|Comment"
msgstr "" msgstr ""
msgid "ProjectService|Comment will be posted on each event"
msgstr ""
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}" msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr "" msgstr ""
@ -18211,6 +18241,9 @@ msgstr ""
msgid "Provider" msgid "Provider"
msgstr "" msgstr ""
msgid "Proxy support for this API is not available currently"
msgstr ""
msgid "Pseudonymizer data collection" msgid "Pseudonymizer data collection"
msgstr "" msgstr ""
@ -24212,6 +24245,9 @@ msgstr ""
msgid "Unreachable" msgid "Unreachable"
msgstr "" msgstr ""
msgid "Unrecognized cluster type"
msgstr ""
msgid "Unresolve" msgid "Unresolve"
msgstr "" msgstr ""

View File

@ -262,6 +262,17 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to match_array([public_project]) } it { is_expected.to match_array([public_project]) }
end 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 describe 'sorting' do
let(:params) { { sort: 'name_asc' } } let(:params) { { sort: 'name_asc' } }

View File

@ -147,6 +147,20 @@ describe('DynamicField', () => {
.text(), .text(),
).toBe(defaultProps.help); ).toBe(defaultProps.help);
}); });
it('renders description with help text as HTML', () => {
const helpHTML = 'The <strong>URL</strong> of the project';
createComponent({
help: helpHTML,
});
expect(
findGlFormGroup()
.find('small')
.html(),
).toContain(helpHTML);
});
}); });
describe('label text', () => { describe('label text', () => {

View File

@ -7,9 +7,4 @@ describe ServicesHelper do
it { expect(event_action_title('comment')).to eq 'Comment' } it { expect(event_action_title('comment')).to eq 'Comment' }
it { expect(event_action_title('something')).to eq 'Something' } it { expect(event_action_title('something')).to eq 'Something' }
end 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 end

View File

@ -234,5 +234,118 @@ describe Gitlab::Diff::PositionTracer::ImageStrategy do
end end
end 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
end end

View File

@ -1801,5 +1801,143 @@ describe Gitlab::Diff::PositionTracer::LineStrategy do
end end
end 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
end end

View File

@ -47,8 +47,8 @@ describe Ci::Bridge do
CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA 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_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG
CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PIPELINE_IID CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE
CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED
] ]

View File

@ -2380,6 +2380,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false }, { 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_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_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_URL', value: project.web_url, public: true, masked: false },
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', 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 }, { key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false },

View File

@ -130,6 +130,24 @@ describe GlobalPolicy do
end end
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| shared_examples 'access allowed when terms accepted' do |ability|
it { is_expected.not_to be_allowed(ability) } it { is_expected.not_to be_allowed(ability) }

View File

@ -584,6 +584,85 @@ describe API::Projects do
end end
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 context 'with keyset pagination' do
let(:current_user) { user } let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3] } let(:projects) { [public_project, project, project2, project3] }

View File

@ -29,6 +29,10 @@ module TestEnv
'gitattributes' => '5a62481', 'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455', 'expand-collapse-diffs' => '4842455',
'symlink-expand-diff' => '81e6355', '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-files' => '025db92',
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'pages-deploy' => '7897d5b', 'pages-deploy' => '7897d5b',