summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js84
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js45
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/helpers/lazy_image_tag_helper.rb26
-rw-r--r--app/models/container_repository.rb11
-rw-r--r--app/models/key.rb8
-rw-r--r--app/services/quick_actions/interpret_service.rb6
-rw-r--r--app/services/service_ping/submit_service.rb28
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/workers/container_registry/migration/guard_worker.rb8
13 files changed, 211 insertions, 47 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
index cd5cfb6837c..23f14bea4e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
@@ -10,6 +10,8 @@ export const i18n = {
label: s__('Reports|Test summary'),
loading: s__('Reports|Test summary results are loading'),
error: s__('Reports|Test summary failed to load results'),
+ newHeader: s__('Reports|New'),
+ fixedHeader: s__('Reports|Fixed'),
fullReport: s__('Reports|Full report'),
noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`),
@@ -36,4 +38,32 @@ export const i18n = {
sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }),
headReportParsingError: s__('Reports|Head report parsing error:'),
baseReportParsingError: s__('Reports|Base report parsing error:'),
+
+ recentFailureSummary: (recentlyFailed, failed) => {
+ if (failed < 2) {
+ return sprintf(
+ s__(
+ 'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days',
+ ),
+ { recentlyFailed, failed },
+ );
+ }
+ return sprintf(
+ n__(
+ 'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days',
+ 'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days',
+ recentlyFailed,
+ ),
+ { recentlyFailed, failed },
+ );
+ },
+ recentFailureCount: (recentFailures) =>
+ sprintf(
+ n__(
+ 'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
+ 'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
+ recentFailures.count,
+ ),
+ recentFailures,
+ ),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
index 65d9257903f..577b2cbfc5c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
@@ -1,7 +1,13 @@
import { uniqueId } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '../../constants';
-import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils';
+import {
+ summaryTextBuilder,
+ reportTextBuilder,
+ reportSubTextBuilder,
+ countRecentlyFailedTests,
+ recentFailuresTextBuilder,
+} from './utils';
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
@@ -18,7 +24,10 @@ export default {
if (data.hasSuiteError) {
return this.$options.i18n.error;
}
- return summaryTextBuilder(this.$options.i18n.label, data.summary);
+ return {
+ subject: summaryTextBuilder(this.$options.i18n.label, data.summary),
+ meta: recentFailuresTextBuilder(data.summary),
+ };
},
statusIcon(data) {
if (data.parsingInProgress) {
@@ -50,6 +59,10 @@ export default {
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === 204,
...data,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(data.suites),
+ ...data.summary,
+ },
},
};
});
@@ -66,17 +79,66 @@ export default {
}
return EXTENSION_ICONS.success;
},
- prepareReports() {
- return this.collapsedData.suites.map((suite) => {
+ testHeader(test, sectionHeader, index) {
+ const headers = [];
+ if (index === 0) {
+ headers.push(sectionHeader);
+ }
+ if (test.recent_failures?.count && test.recent_failures?.base_branch) {
+ headers.push(i18n.recentFailureCount(test.recent_failures));
+ }
+ return headers;
+ },
+ mapTestAsChild({ iconName, sectionHeader }) {
+ return (test, index) => {
return {
- id: uniqueId('suite-'),
- text: reportTextBuilder(suite),
- subtext: reportSubTextBuilder(suite),
- icon: {
- name: this.suiteIcon(suite),
- },
+ id: uniqueId('test-'),
+ header: this.testHeader(test, sectionHeader, index),
+ icon: { name: iconName },
+ text: test.name,
};
- });
+ };
+ },
+ prepareReports() {
+ return this.collapsedData.suites
+ .map((suite) => {
+ return {
+ ...suite,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suite),
+ ...suite.summary,
+ },
+ };
+ })
+ .map((suite) => {
+ return {
+ id: uniqueId('suite-'),
+ text: reportTextBuilder(suite),
+ subtext: reportSubTextBuilder(suite),
+ icon: {
+ name: this.suiteIcon(suite),
+ },
+ children: [
+ ...[...suite.new_failures, ...suite.new_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.newHeader,
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.existing_failures, ...suite.existing_errors].map(
+ this.mapTestAsChild({
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.resolved_failures, ...suite.resolved_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.fixedHeader,
+ iconName: EXTENSION_ICONS.success,
+ }),
+ ),
+ ],
+ };
+ });
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
index a74ed20362f..9e4b0ac581c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -43,13 +43,42 @@ export const reportTextBuilder = ({ name = '', summary = {}, status }) => {
return i18n.summaryText(name, resultsString);
};
-export const reportSubTextBuilder = ({ suite_errors }) => {
- const errors = [];
- if (suite_errors?.head) {
- errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
- }
- if (suite_errors?.base) {
- errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
+export const recentFailuresTextBuilder = (summary = {}) => {
+ const { failed, recentlyFailed } = summary;
+ if (!failed || !recentlyFailed) return '';
+
+ return i18n.recentFailureSummary(recentlyFailed, failed);
+};
+
+export const reportSubTextBuilder = ({ suite_errors, summary }) => {
+ if (suite_errors?.head || suite_errors?.base) {
+ const errors = [];
+ if (suite_errors?.head) {
+ errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
+ }
+ if (suite_errors?.base) {
+ errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
+ }
+ return errors.join('<br />');
}
- return errors.join('<br />');
+ return recentFailuresTextBuilder(summary);
+};
+
+export const countRecentlyFailedTests = (subject) => {
+ // handle either a single report or an array of reports
+ const reports = !subject.length ? [subject] : subject;
+
+ return reports
+ .map((report) => {
+ return (
+ [report.new_failures, report.existing_failures, report.resolved_failures]
+ // only count tests which have failed more than once
+ .map(
+ (failureArray) =>
+ failureArray.filter((failure) => failure.recent_failures?.count > 1).length,
+ )
+ .reduce((total, count) => total + count, 0)
+ );
+ })
+ .reduce((total, count) => total + count, 0);
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 4b3ad288768..04f71e2b185 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -195,6 +195,9 @@ export default {
shouldRenderTestReport() {
return Boolean(this.mr?.testResultsPath);
},
+ shouldRenderRefactoredTestReport() {
+ return window.gon?.features?.refactorMrWidgetTestSummary;
+ },
mergeError() {
let { mergeError } = this.mr;
@@ -512,7 +515,7 @@ export default {
}
},
registerTestReportExtension() {
- if (this.shouldRenderTestReport && this.shouldShowExtension) {
+ if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) {
registerExtension(testReportExtension);
}
},
@@ -588,7 +591,7 @@ export default {
/>
<grouped-test-reports-app
- v-if="mr.testResultsPath && !shouldShowExtension"
+ v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport"
class="js-reports-container"
:endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 03bb132fe47..47f82471937 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml)
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:refactor_mr_widget_test_summary, project, default_enabled: :yaml)
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index d0bdaaae5f8..10d603ef5d3 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -8,8 +8,11 @@ module LazyImageTagHelper
end
# Override the default ActionView `image_tag` helper to support lazy-loading
+ # accept :auto_dark boolean to enable automatic dark variant of the image
+ # (see: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2698)
+ # accept :dark_variant path to be used as a source when dark mode is enabled
def image_tag(source, options = {})
- source = options[:dark_variant] if options[:dark_variant] && user_application_dark_mode?
+ source, options = prepare_dark_variant(source, options)
options = options.symbolize_keys
unless options.delete(:lazy) == false
@@ -29,4 +32,25 @@ module LazyImageTagHelper
# Required for Banzai::Filter::ImageLazyLoadFilter
module_function :placeholder_image # rubocop: disable Style/AccessModifierDeclarations
+
+ private
+
+ def prepare_dark_variant(source, options)
+ dark_variant = options.delete(:dark_variant)
+ auto_dark = options.delete(:auto_dark)
+
+ if dark_variant && auto_dark
+ raise ArgumentError, "dark_variant and auto_dark are mutually exclusive"
+ end
+
+ if (auto_dark || dark_variant) && user_application_dark_mode?
+ if auto_dark
+ options[:class] = 'gl-dark-invert-keep-hue'
+ elsif dark_variant
+ source = dark_variant
+ end
+ end
+
+ [source, options]
+ end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 78bd520d5d5..92f0b978788 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -43,7 +43,8 @@ class ContainerRepository < ApplicationRecord
migration_canceled: 4,
not_found: 5,
native_import: 6,
- migration_forced_canceled: 7
+ migration_forced_canceled: 7,
+ migration_canceled_by_registry: 8
}
delegate :client, :gitlab_api_client, to: :registry
@@ -214,7 +215,7 @@ class ContainerRepository < ApplicationRecord
container_repository.migration_skipped_at = Time.zone.now
end
- before_transition any => %i[import_done import_aborted] do |container_repository|
+ before_transition any => %i[import_done import_aborted import_skipped] do |container_repository|
container_repository.run_after_commit do
::ContainerRegistry::Migration::EnqueuerWorker.perform_async
end
@@ -328,7 +329,7 @@ class ContainerRepository < ApplicationRecord
when 'import_canceled', 'pre_import_canceled'
return if import_skipped?
- skip_import(reason: :migration_canceled)
+ skip_import(reason: :migration_canceled_by_registry)
when 'import_complete'
finish_import
when 'import_failed'
@@ -376,6 +377,10 @@ class ContainerRepository < ApplicationRecord
migration_retries_count >= ContainerRegistry::Migration.max_retries
end
+ def nearing_or_exceeded_retry_limit?
+ migration_retries_count >= ContainerRegistry::Migration.max_retries - 1
+ end
+
def last_import_step_done_at
[migration_pre_import_done_at, migration_import_done_at, migration_aborted_at, migration_skipped_at].compact.max
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 42ea0f29171..07d5b1eea3a 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -24,15 +24,9 @@ class Key < ApplicationRecord
length: { maximum: 5000 },
format: { with: /\A(#{Gitlab::SSHPublicKey.supported_algorithms.join('|')})/ }
- validates :fingerprint,
- uniqueness: true,
- presence: { message: 'cannot be generated' },
- unless: -> { Gitlab::FIPS.enabled? }
-
validates :fingerprint_sha256,
uniqueness: true,
- presence: { message: 'cannot be generated' },
- if: -> { Gitlab::FIPS.enabled? }
+ presence: { message: 'cannot be generated' }
validate :key_meets_restrictions
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 47f4b9c6898..30d7093b8a6 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -83,8 +83,10 @@ module QuickActions
args.map! { _1.gsub(/\\_/, '_') }
usernames = (args - ['me']).map { _1.delete_prefix('@') }
found = User.by_username(usernames).to_a.select { can?(:read_user, _1) }
- found_names = found.map(&:username).to_set
- missing = args.reject { |arg| arg == 'me' || found_names.include?(arg.delete_prefix('@')) }.map { "'#{_1}'" }
+ found_names = found.map(&:username).map(&:downcase).to_set
+ missing = args.reject do |arg|
+ arg == 'me' || found_names.include?(arg.downcase.delete_prefix('@'))
+ end.map { "'#{_1}'" }
failed_parse(format(_("Failed to find users for %{missing}"), missing: missing.to_sentence)) if missing.present?
diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb
index c8733bc2f11..ed6eb071f32 100644
--- a/app/services/service_ping/submit_service.rb
+++ b/app/services/service_ping/submit_service.rb
@@ -6,6 +6,7 @@ module ServicePing
STAGING_BASE_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org'
USAGE_DATA_PATH = 'usage_data'
ERROR_PATH = 'usage_ping_errors'
+ METADATA_PATH = 'usage_ping_metadata'
SubmissionError = Class.new(StandardError)
@@ -31,7 +32,7 @@ module ServicePing
message: e.message,
elapsed: (Time.current - start).round(1)
}
- submit_payload({ error: error_payload }, url: error_url)
+ submit_payload({ error: error_payload }, path: ERROR_PATH)
usage_data = Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
response = submit_usage_data_payload(usage_data)
@@ -48,21 +49,30 @@ module ServicePing
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
DevopsReportService.new(response).execute
end
- end
- def url
- URI.join(base_url, USAGE_DATA_PATH)
- end
+ return unless Feature.enabled?(:measure_service_ping_metric_collection, default_enabled: :yaml)
- def error_url
- URI.join(base_url, ERROR_PATH)
+ submit_payload({ metadata: { metrics: metrics_collection_time(usage_data) } }, path: METADATA_PATH)
end
private
- def submit_payload(payload, url: self.url)
+ def metrics_collection_time(payload, parents = [])
+ return [] unless payload.is_a?(Hash)
+
+ payload.flat_map do |key, metric_value|
+ key_path = parents.dup.append(key)
+ if metric_value.respond_to?(:duration)
+ { name: key_path.join('.'), time_elapsed: metric_value.duration }
+ else
+ metrics_collection_time(metric_value, key_path)
+ end
+ end
+ end
+
+ def submit_payload(payload, path: USAGE_DATA_PATH)
Gitlab::HTTP.post(
- url,
+ URI.join(base_url, path),
body: payload.to_json,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 1a2f770cd59..6ecc050fa25 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -34,7 +34,7 @@
= render 'groups/settings/ip_restriction_registration_features_cta', f: f
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
- - if Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml)
+ - if @group.licensed_feature_available?(:group_wikis) && Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml)
= render_if_exists 'groups/settings/wiki', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index d0c4fb2432c..d199c8e71a2 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -10,7 +10,7 @@
.row.empty-state.merge-requests
.col-12
.svg-content
- = image_tag 'illustrations/merge_requests.svg'
+ = image_tag 'illustrations/merge_requests.svg', { auto_dark: true }
.col-12
.text-content
- if has_filter_bar_param?
diff --git a/app/workers/container_registry/migration/guard_worker.rb b/app/workers/container_registry/migration/guard_worker.rb
index bab6b8c2a72..e5b3bd908c1 100644
--- a/app/workers/container_registry/migration/guard_worker.rb
+++ b/app/workers/container_registry/migration/guard_worker.rb
@@ -93,7 +93,7 @@ module ContainerRegistry
end
def long_running_migration_threshold
- @threshold ||= 30.minutes.ago
+ @threshold ||= 10.minutes.ago
end
def cancel_long_running_migration(repository)
@@ -101,7 +101,11 @@ module ContainerRegistry
case result[:status]
when :ok
- repository.skip_import(reason: :migration_canceled)
+ if repository.nearing_or_exceeded_retry_limit?
+ repository.skip_import(reason: :migration_canceled)
+ else
+ repository.abort_import
+ end
when :bad_request
repository.reconcile_import_status(result[:state]) do
repository.abort_import