summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-15 09:08:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-15 09:08:57 +0000
commit45a8c43afe8a17de19a92708b380b29b6ae04ce6 (patch)
tree4104e6ac741fbbdeefe9b8b699650a06c14e9056 /app
parent6bc327a3491069240bd73cc83e17b3078c4148b0 (diff)
downloadgitlab-ce-45a8c43afe8a17de19a92708b380b29b6ae04ce6.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue51
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js23
-rw-r--r--app/assets/javascripts/pages/projects/project.js7
-rw-r--r--app/controllers/groups/milestones_controller.rb28
-rw-r--r--app/controllers/projects/milestones_controller.rb28
-rw-r--r--app/finders/template_finder.rb15
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/create.rb2
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/delete.rb2
-rw-r--r--app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb1
-rw-r--r--app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb20
-rw-r--r--app/graphql/resolvers/projects/commit_parent_names_resolver.rb33
-rw-r--r--app/graphql/resolvers/projects/commit_references_resolver.rb29
-rw-r--r--app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb25
-rw-r--r--app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb20
-rw-r--r--app/graphql/types/commit_references_type.rb67
-rw-r--r--app/graphql/types/mutation_type.rb10
-rw-r--r--app/graphql/types/permission_types/issue.rb2
-rw-r--r--app/graphql/types/project_statistics_redirect_type.rb27
-rw-r--r--app/graphql/types/project_type.rb32
-rw-r--r--app/graphql/types/projects/commit_parent_names_type.rb1
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb4
-rw-r--r--app/models/commit.rb35
-rw-r--r--app/models/container_registry/data_repair_detail.rb6
-rw-r--r--app/models/project.rb6
-rw-r--r--app/services/jira_connect/sync_service.rb4
-rw-r--r--app/views/clusters/clusters/_integrations.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml6
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml7
-rw-r--r--app/views/projects/milestones/_form.html.haml6
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/shared/_captcha_check.html.haml4
-rw-r--r--app/views/shared/_model_version_conflict.html.haml6
-rw-r--r--app/views/shared/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/shared/integrations/prometheus/_custom_metrics.html.haml2
-rw-r--r--app/views/shared/integrations/prometheus/_metrics.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml10
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/container_registry/cleanup_worker.rb24
-rw-r--r--app/workers/container_registry/record_data_repair_detail_worker.rb87
43 files changed, 484 insertions, 179 deletions
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index de7240009bc..6d914fe8361 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -185,6 +185,6 @@ export const I18N_ALERT_SETTINGS_FORM = {
export const NO_ISSUE_TEMPLATE_SELECTED = { value: '', text: __('No template selected') };
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
- '/help/operations/metrics/alerts#trigger-actions-from-alerts';
+ '/help/operations/incident_management/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#create-an-issue-template';
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue
index c23a0b866d3..b4b468987d8 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue
@@ -1,12 +1,14 @@
<script>
-import { GlFormGroup, GlAccordionItem, GlFormInput } from '@gitlab/ui';
+import { GlFormGroup, GlAccordionItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { i18n } from '../constants';
export default {
i18n,
+ placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT,
components: {
GlAccordionItem,
GlFormInput,
+ GlFormTextarea,
GlFormGroup,
},
props: {
@@ -15,31 +17,34 @@ export default {
required: true,
},
},
+ computed: {
+ imageEntryPoint() {
+ return this.job.image.entrypoint.join('\n');
+ },
+ },
};
</script>
<template>
<gl-accordion-item :title="$options.i18n.IMAGE">
- <div class="gl-display-flex">
- <gl-form-group
- class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3"
- :label="$options.i18n.IMAGE_NAME"
- >
- <gl-form-input
- :value="job.image.name"
- data-testid="image-name-input"
- @input="$emit('update-job', 'image.name', $event)"
- />
- </gl-form-group>
- <gl-form-group
- class="gl-flex-grow-1 gl-flex-basis-half"
- :label="$options.i18n.IMAGE_ENTRYPOINT"
- >
- <gl-form-input
- :value="job.image.entrypoint.join(' ')"
- data-testid="image-entrypoint-input"
- @input="$emit('update-job', 'image.entrypoint', $event.split(' '))"
- />
- </gl-form-group>
- </div>
+ <gl-form-group :label="$options.i18n.IMAGE_NAME">
+ <gl-form-input
+ :value="job.image.name"
+ data-testid="image-name-input"
+ @input="$emit('update-job', 'image.name', $event)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.IMAGE_ENTRYPOINT"
+ :description="$options.i18n.ARRAY_FIELD_DESCRIPTION"
+ class="gl-mb-0"
+ >
+ <gl-form-textarea
+ :no-resize="false"
+ :placeholder="$options.placeholderText"
+ data-testid="image-entrypoint-input"
+ :value="imageEntryPoint"
+ @input="$emit('update-job', 'image.entrypoint', $event.split('\n'))"
+ />
+ </gl-form-group>
</gl-accordion-item>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
index b49355d539c..511003d3ad4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
@@ -39,6 +39,7 @@ export default {
availableStages: {
type: Array,
required: true,
+ default: () => [],
},
},
};
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue
index fd669adb077..9bada3ef110 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue
@@ -4,7 +4,7 @@ import { i18n } from '../constants';
export default {
i18n,
- placeholderText: i18n.ENTRPOINT_PLACEHOLDER_TEXT,
+ placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT,
components: {
GlAccordionItem,
GlFormGroup,
@@ -38,7 +38,7 @@ export default {
},
serviceEntryPoint(service) {
const { entrypoint = [''] } = service;
- return entrypoint.join(',');
+ return entrypoint.join('\n');
},
},
};
@@ -75,7 +75,7 @@ export default {
:placeholder="$options.placeholderText"
:data-testid="`service-entrypoint-input-${index}`"
:value="serviceEntryPoint(service)"
- @input="$emit('update-job', `services[${index}].entrypoint`, $event.split(','))"
+ @input="$emit('update-job', `services[${index}].entrypoint`, $event.split('\n'))"
/>
</gl-form-group>
</div>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
index d9d1a31ab6d..e93a9e84302 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
@@ -1,4 +1,4 @@
-import { __, s__, sprintf } from '~/locale';
+import { __, s__ } from '~/locale';
export const DRAWER_CONTAINER_CLASS = '.content-wrapper';
@@ -87,14 +87,8 @@ export const JOB_TEMPLATE = {
],
};
-export const ENTRYPOINT_PLACEHOLDER_EXAMPLE =
- "['/bin/bash','-c','ln -snf /bin/bash /bin/sh && /bin/bash -c $0']";
-
-export const ENTRYPOINT_PLACEHOLDER_INPUT =
- 'bin/bash,-c,ln -snf /bin/bash /bin/sh && /bin/bash -c $0';
-
export const i18n = {
- ARRAY_FIELD_DESCRIPTION: s__('JobAssistant|Please use "," to separate array type fields.'),
+ ARRAY_FIELD_DESCRIPTION: s__('JobAssistant|Please separate array type fields with new lines'),
INPUT_FORMAT: s__('JobAssistant|Input format'),
ADD_JOB: s__('JobAssistant|Add job'),
SCRIPT: s__('JobAssistant|Script'),
@@ -117,17 +111,8 @@ export const i18n = {
ALLOW_FAILURE: s__('JobAssistant|Allow failure'),
INVALID_START_IN: s__('JobAssistant|Error - Valid value is between 1 second and 1 week'),
ADD_SERVICE: s__('JobAssistant|Add service'),
- SERVICE: s__('JobAssistant|Service'),
+ SERVICE: s__('JobAssistant|Services'),
SERVICE_NAME: s__('JobAssistant|Service name (optional)'),
SERVICE_ENTRYPOINT: s__('JobAssistant|Service entrypoint (optional)'),
- ENTRPOINT_PLACEHOLDER_TEXT: sprintf(
- s__(
- 'JobAssistant|Example: %{entrypointPlaceholderExample}, Input format: %{entrypointPlaceholderInput}',
- ),
- {
- entrypointPlaceholderExample: ENTRYPOINT_PLACEHOLDER_EXAMPLE,
- entrypointPlaceholderInput: ENTRYPOINT_PLACEHOLDER_INPUT,
- },
- false,
- ),
+ ENTRYPOINT_PLACEHOLDER_TEXT: s__('JobAssistant|Please enter the parameters.'),
};
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index e6ee6b702bb..c566490dcb5 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -25,6 +25,13 @@ export default class Project {
$(this).parents('.auto-devops-implicitly-enabled-banner').remove();
return e.preventDefault();
});
+ $('.hide-mobile-devops-promo').on('click', function (e) {
+ const projectId = $(this).data('project-id');
+ const cookieKey = `hide_mobile_devops_promo_${projectId}`;
+ setCookie(cookieKey, 'false');
+ $(this).parents('#mobile-devops-promo-banner').remove();
+ return e.preventDefault();
+ });
}
static changeProject(url) {
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 494b8c5621d..903c8c214ae 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -45,6 +45,24 @@ class Groups::MilestonesController < Groups::ApplicationController
Milestones::UpdateService.new(@milestone.parent, current_user, milestone_params).execute(@milestone)
redirect_to milestone_path(@milestone)
+ rescue ActiveRecord::StaleObjectError
+ respond_to do |format|
+ format.html do
+ @conflict = true
+ render :edit
+ end
+
+ format.json do
+ render json: {
+ errors: [
+ format(
+ _("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength
+ model_name: _('milestone')
+ )
+ ]
+ }, status: :conflict
+ end
+ end
end
def destroy
@@ -63,7 +81,15 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
+ params.require(:milestone)
+ .permit(
+ :description,
+ :due_date,
+ :lock_version,
+ :start_date,
+ :state_event,
+ :title
+ )
end
def milestones
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 78108cf3478..569a514b23b 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -85,6 +85,24 @@ class Projects::MilestonesController < Projects::ApplicationController
end
end
end
+ rescue ActiveRecord::StaleObjectError
+ respond_to do |format|
+ format.html do
+ @conflict = true
+ render :edit
+ end
+
+ format.json do
+ render json: {
+ errors: [
+ format(
+ _("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."),
+ model_name: _('milestone')
+ )
+ ]
+ }, status: :conflict
+ end
+ end
end
def promote
@@ -152,7 +170,15 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
+ params.require(:milestone)
+ .permit(
+ :description,
+ :due_date,
+ :lock_version,
+ :start_date,
+ :state_event,
+ :title
+ )
end
def search_params
diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb
index b82b601541c..c6c5c30cbf7 100644
--- a/app/finders/template_finder.rb
+++ b/app/finders/template_finder.rb
@@ -16,16 +16,27 @@ class TemplateFinder
def build(type, project, params = {})
if type.to_s == 'licenses'
LicenseTemplateFinder.new(project, params) # rubocop: disable CodeReuse/Finder
- else
+ elsif type_allowed?(type)
new(type, project, params)
end
end
def all_template_names(project, type)
- return {} if !VENDORED_TEMPLATES.key?(type.to_s) && type.to_s != 'licenses'
+ return {} unless type_allowed?(type)
build(type, project).template_names
end
+
+ def type_allowed?(type)
+ case type.to_s
+ when 'licenses'
+ true
+ when 'metrics_dashboard_ymls'
+ !Feature.enabled?(:remove_monitor_metrics)
+ else
+ VENDORED_TEMPLATES.key?(type)
+ end
+ end
end
attr_reader :type, :project, :params
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
index 225d313c487..296efa19bb7 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
@@ -75,6 +75,8 @@ module Mutations
private
def ready?(**args)
+ raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics)
+
# Raise error if both cluster_id and environment_id are present or neither is present
unless args[:cluster_id].present? ^ args[:environment_id].present?
raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
index 0ee2791f78b..32047cda213 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb
@@ -14,6 +14,8 @@ module Mutations
description: 'Global ID of the annotation to delete.'
def resolve(id:)
+ raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics)
+
annotation = authorized_find!(id: id)
result = ::Metrics::Dashboard::Annotations::DeleteService.new(context[:current_user], annotation).execute
diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
index 9d6b0486c04..aad9bbebafb 100644
--- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
@@ -17,6 +17,7 @@ module Resolvers
alias_method :dashboard, :object
def resolve(**args)
+ return if Feature.enabled?(:remove_monitor_metrics)
return [] unless dashboard
::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute
diff --git a/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb
deleted file mode 100644
index 7e2661f3f77..00000000000
--- a/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- module Projects
- class BranchesTippingAtCommitResolver < RefTippingAtCommitResolver
- MAX_LIMIT = 100
-
- calls_gitaly!
-
- type ::Types::Projects::CommitParentNamesType, null: true
-
- # the methode ref_prefix is implemented
- # because this class is prepending Resolver::CommitParentNamesResolver module
- # through it's parent ::Resolvers::RefTippingAtCommitResolver
- def ref_prefix
- Gitlab::Git::BRANCH_REF_PREFIX
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/projects/commit_parent_names_resolver.rb b/app/graphql/resolvers/projects/commit_parent_names_resolver.rb
deleted file mode 100644
index f52776d715a..00000000000
--- a/app/graphql/resolvers/projects/commit_parent_names_resolver.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- module Projects
- module CommitParentNamesResolver
- extend ActiveSupport::Concern
-
- prepended do
- argument :commit_sha, GraphQL::Types::String,
- required: true,
- description: 'Project commit SHA identifier. For example, `287774414568010855642518513f085491644061`.'
-
- argument :limit, GraphQL::Types::Int,
- required: false,
- description: 'Number of branch names to return.'
-
- alias_method :project, :object
- end
-
- def compute_limit(limit)
- max = self.class::MAX_LIMIT
-
- limit ? [limit, max].min : max
- end
-
- def get_tipping_refs(project, sha, limit: 0)
- # the methode ref_prefix needs to be implemented in all classes prepending this module
- refs = project.repository.refs_by_oid(oid: sha, ref_patterns: [ref_prefix], limit: limit)
- refs.map { |n| n.delete_prefix(ref_prefix) }
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/projects/commit_references_resolver.rb b/app/graphql/resolvers/projects/commit_references_resolver.rb
new file mode 100644
index 00000000000..ca25bad468c
--- /dev/null
+++ b/app/graphql/resolvers/projects/commit_references_resolver.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Projects
+ class CommitReferencesResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ argument :commit_sha, GraphQL::Types::String,
+ required: true,
+ description: 'Project commit SHA identifier. For example, `287774414568010855642518513f085491644061`.'
+
+ authorize :read_commit
+
+ alias_method :project, :object
+
+ calls_gitaly!
+
+ type ::Types::CommitReferencesType, null: true
+
+ def resolve(commit_sha:)
+ authorized_find!(oid: commit_sha)
+ end
+
+ def find_object(oid:)
+ project.repository.commit(oid)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb
deleted file mode 100644
index 3259a29ac9c..00000000000
--- a/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- module Projects
- class RefTippingAtCommitResolver < BaseResolver
- include Gitlab::Graphql::Authorize::AuthorizeResource
- prepend CommitParentNamesResolver
-
- type ::Types::Projects::CommitParentNamesType, null: true
-
- authorize :read_code
-
- def resolve(commit_sha:, limit: nil)
- final_limit = compute_limit(limit)
-
- names = get_tipping_refs(project, commit_sha, limit: final_limit)
-
- {
- names: names,
- total_count: nil
- }
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb
deleted file mode 100644
index 78ee9c997d5..00000000000
--- a/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- module Projects
- class TagsTippingAtCommitResolver < RefTippingAtCommitResolver
- MAX_LIMIT = 100
-
- calls_gitaly!
-
- type ::Types::Projects::CommitParentNamesType, null: true
-
- # the methode ref_prefix is implemented
- # because this class is prepending Resolver::CommitParentNamesResolver module
- # through it's parent ::Resolvers::RefTippingAtCommitResolver
- def ref_prefix
- Gitlab::Git::TAG_REF_PREFIX
- end
- end
- end
-end
diff --git a/app/graphql/types/commit_references_type.rb b/app/graphql/types/commit_references_type.rb
new file mode 100644
index 00000000000..2844a552f3e
--- /dev/null
+++ b/app/graphql/types/commit_references_type.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Types
+ class CommitReferencesType < BaseObject
+ graphql_name 'CommitReferences'
+
+ authorize :read_commit
+
+ def self.field_for_tipping_refs(field_name, field_description)
+ field field_name, ::Types::Projects::CommitParentNamesType,
+ null: true,
+ calls_gitaly: true,
+ description: field_description do
+ argument :limit, GraphQL::Types::Int,
+ required: true,
+ default_value: 100,
+ description: 'Number of ref names to return.',
+ validates: { numericality: { within: 1..1000 } }
+ end
+ end
+
+ def self.field_for_containing_refs(field_name, field_description)
+ field field_name, ::Types::Projects::CommitParentNamesType,
+ null: true,
+ calls_gitaly: true,
+ description: field_description do
+ argument :exclude_tipped, GraphQL::Types::Boolean,
+ required: true,
+ default_value: false,
+ description: 'Exclude tipping refs. WARNING: This argument can be confusing, if there is a limit.
+ for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
+ then the method will only 3 refs, even though there is more.'
+ # rubocop: disable GraphQL/ArgumentUniqueness
+ argument :limit, GraphQL::Types::Int,
+ required: true,
+ default_value: 100,
+ description: 'Number of ref names to return.',
+ validates: { numericality: { within: 1..1000 } }
+ # rubocop: enable GraphQL/ArgumentUniqueness
+ end
+ end
+
+ field_for_tipping_refs :tipping_tags, "Get tag names tipping at a given commit."
+
+ field_for_tipping_refs :tipping_branches, "Get branch names tipping at a given commit."
+
+ field_for_containing_refs :containing_tags, "Get tag names containing a given commit."
+
+ field_for_containing_refs :containing_branches, "Get branch names containing a given commit."
+
+ def tipping_tags(limit:)
+ { names: object.tipping_tags(limit: limit) }
+ end
+
+ def tipping_branches(limit:)
+ { names: object.tipping_branches(limit: limit) }
+ end
+
+ def containing_tags(limit:, exclude_tipped:)
+ { names: object.tags_containing(limit: limit, exclude_tipped: exclude_tipped) }
+ end
+
+ def containing_branches(limit:, exclude_tipped:)
+ { names: object.branches_containing(limit: limit, exclude_tipped: exclude_tipped) }
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 64f7daf09a8..7e436d74dcf 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -86,8 +86,14 @@ module Types
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::SetReviewers
mount_mutation Mutations::MergeRequests::ReviewerRereview
- mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
- mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete
+ mount_mutation Mutations::Metrics::Dashboard::Annotations::Create, deprecated: {
+ reason: 'Underlying feature was removed in 16.0',
+ milestone: '16.0'
+ }
+ mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete, deprecated: {
+ reason: 'Underlying feature was removed in 16.0',
+ milestone: '16.0'
+ }
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb
index b38971b64cd..a76dc88adfc 100644
--- a/app/graphql/types/permission_types/issue.rb
+++ b/app/graphql/types/permission_types/issue.rb
@@ -8,7 +8,7 @@ module Types
abilities :read_issue, :admin_issue, :update_issue, :reopen_issue,
:read_design, :create_design, :destroy_design,
- :create_note
+ :create_note, :update_design
end
end
end
diff --git a/app/graphql/types/project_statistics_redirect_type.rb b/app/graphql/types/project_statistics_redirect_type.rb
new file mode 100644
index 00000000000..c8fec0a54c4
--- /dev/null
+++ b/app/graphql/types/project_statistics_redirect_type.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class ProjectStatisticsRedirectType < BaseObject
+ graphql_name 'ProjectStatisticsRedirect'
+
+ field :repository, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for repository.'
+
+ field :wiki, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for wiki.'
+
+ field :build_artifacts, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for job_artifacts.'
+
+ field :packages, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for packages.'
+
+ field :snippets, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for snippets.'
+
+ field :container_registry, GraphQL::Types::String, null: false,
+ description: 'Redirection Route for container_registry.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 6dfbdf765c8..f8a516501c3 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -214,6 +214,11 @@ module Types
null: true,
description: 'Statistics of the project.'
+ field :statistics_details_paths, Types::ProjectStatisticsRedirectType,
+ null: true,
+ description: 'Redirects for Statistics of the project.',
+ calls_gitaly: true
+
field :repository, Types::RepositoryType,
null: true,
description: 'Git repository of the project.'
@@ -612,15 +617,11 @@ module Types
authorize: :read_cycle_analytics,
alpha: { milestone: '15.10' }
- field :tags_tipping_at_commit, ::Types::Projects::CommitParentNamesType,
- null: true,
- resolver: Resolvers::Projects::TagsTippingAtCommitResolver,
- description: "Get tag names tipping at a given commit."
-
- field :branches_tipping_at_commit, ::Types::Projects::CommitParentNamesType,
- null: true,
- resolver: Resolvers::Projects::BranchesTippingAtCommitResolver,
- description: "Get branch names tipping at a given commit."
+ field :commit_references, ::Types::CommitReferencesType,
+ null: true,
+ resolver: Resolvers::Projects::CommitReferencesResolver,
+ alpha: { milestone: '16.0' },
+ description: "Get tag names containing a given commit."
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
@@ -730,6 +731,19 @@ module Types
end
end
+ def statistics_details_paths
+ root_ref = project.repository.root_ref || project.default_branch_or_main
+
+ {
+ repository: Gitlab::Routing.url_helpers.project_tree_url(project, root_ref),
+ wiki: Gitlab::Routing.url_helpers.project_wikis_pages_url(project),
+ build_artifacts: Gitlab::Routing.url_helpers.project_artifacts_url(project),
+ packages: Gitlab::Routing.url_helpers.project_packages_url(project),
+ snippets: Gitlab::Routing.url_helpers.project_snippets_url(project),
+ container_registry: Gitlab::Routing.url_helpers.project_container_registry_index_url(project)
+ }
+ end
+
private
def project
diff --git a/app/graphql/types/projects/commit_parent_names_type.rb b/app/graphql/types/projects/commit_parent_names_type.rb
index 0aa1ca768e9..39f8f1cdd07 100644
--- a/app/graphql/types/projects/commit_parent_names_type.rb
+++ b/app/graphql/types/projects/commit_parent_names_type.rb
@@ -7,7 +7,6 @@ module Types
graphql_name 'CommitParentNames'
field :names, [GraphQL::Types::String], null: true, description: 'Names of the commit parent (branch or tag).'
- field :total_count, GraphQL::Types::Int, null: true, description: 'Total of parent branches or tags.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 32331136e28..1e87d2861d4 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -259,6 +259,14 @@ module ProjectsHelper
cookies["hide_auto_devops_implicitly_enabled_banner_#{project.id}".to_sym].blank?
end
+ def show_mobile_devops_project_promo?(project)
+ return false unless ::Feature.enabled?(:mobile_devops_projects_promo, project)
+
+ return false unless (project.project_setting.target_platforms & ::ProjectSetting::ALLOWED_TARGET_PLATFORMS).any?
+
+ cookies["hide_mobile_devops_promo_#{project.id}".to_sym].blank?
+ end
+
def no_password_message
push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('topics/git/terminology', anchor: 'pull-and-push') }
clone_with_https_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'clone-with-https') }
diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb
index 4b7a178566c..b63f3022198 100644
--- a/app/models/blob_viewer/metrics_dashboard_yml.rb
+++ b/app/models/blob_viewer/metrics_dashboard_yml.rb
@@ -11,6 +11,10 @@ module BlobViewer
self.file_types = %i(metrics_dashboard)
self.binary = false
+ def self.can_render?(blob, verify_binary: true)
+ super && !Feature.enabled?(:remove_monitor_metrics)
+ end
+
def valid?
errors.blank?
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b618845705c..6d17d7f495d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -572,8 +572,43 @@ class Commit
}
end
+ def tipping_branches(limit: 0)
+ tipping_refs(Gitlab::Git::BRANCH_REF_PREFIX, limit: limit)
+ end
+
+ def tipping_tags(limit: 0)
+ tipping_refs(Gitlab::Git::TAG_REF_PREFIX, limit: limit)
+ end
+
+ def branches_containing(limit: 0, exclude_tipped: false)
+ # WARNING: This argument can be confusing, if there is a limit.
+ # for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
+ # then the method will only 3 refs, even though there is more.
+ excluded = exclude_tipped ? tipping_branches : []
+
+ refs = repository.branch_names_contains(id, limit: limit) || []
+ refs - excluded
+ end
+
+ def tags_containing(limit: 0, exclude_tipped: false)
+ # WARNING: This argument can be confusing, if there is a limit.
+ # for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
+ # then the method will only 3 refs, even though there is more.
+ excluded = exclude_tipped ? tipping_tags : []
+
+ refs = repository.tag_names_contains(id, limit: limit) || []
+ refs - excluded
+ end
+
private
+ def tipping_refs(ref_prefix, limit: 0)
+ strong_memoize_with(:tipping_tags, ref_prefix, limit) do
+ refs = repository.refs_by_oid(oid: id, ref_patterns: [ref_prefix], limit: limit)
+ refs.map { |n| n.delete_prefix(ref_prefix) }
+ end
+ end
+
def expire_note_etag_cache_for_related_mrs
MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache)
end
diff --git a/app/models/container_registry/data_repair_detail.rb b/app/models/container_registry/data_repair_detail.rb
index 09e617e69f5..a2616490905 100644
--- a/app/models/container_registry/data_repair_detail.rb
+++ b/app/models/container_registry/data_repair_detail.rb
@@ -2,9 +2,15 @@
module ContainerRegistry
class DataRepairDetail < ApplicationRecord
+ include EachBatch
+
self.table_name = 'container_registry_data_repair_details'
self.primary_key = :project_id
belongs_to :project, optional: false
+
+ enum status: { ongoing: 0, completed: 1, failed: 2 }
+
+ scope :ongoing_since, ->(threshold) { where(status: :ongoing).where('updated_at < ?', threshold) }
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index d8818d9c174..224193fba08 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -188,6 +188,7 @@ class Project < ApplicationRecord
has_one :confluence_integration, class_name: 'Integrations::Confluence'
has_one :custom_issue_tracker_integration, class_name: 'Integrations::CustomIssueTracker'
has_one :datadog_integration, class_name: 'Integrations::Datadog'
+ has_one :container_registry_data_repair_detail, class_name: 'ContainerRegistry::DataRepairDetail'
has_one :discord_integration, class_name: 'Integrations::Discord'
has_one :drone_ci_integration, class_name: 'Integrations::DroneCi'
has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
@@ -740,6 +741,11 @@ class Project < ApplicationRecord
topic ? with_topic(topic) : none
end
+ scope :pending_data_repair_analysis, -> do
+ left_outer_joins(:container_registry_data_repair_detail)
+ .where(container_registry_data_repair_details: { project_id: nil })
+ end
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb
index 92255711399..497c282072d 100644
--- a/app/services/jira_connect/sync_service.rb
+++ b/app/services/jira_connect/sync_service.rb
@@ -31,7 +31,9 @@ module JiraConnect
jira_response: response&.to_json
}
- if response && response['errorMessages'].present?
+ has_errors = response && (response['errorMessage'].present? || response['errorMessages'].present?)
+
+ if has_errors
logger.error(message)
else
logger.info(message)
diff --git a/app/views/clusters/clusters/_integrations.html.haml b/app/views/clusters/clusters/_integrations.html.haml
index 0f62b640b97..4d36c5094a3 100644
--- a/app/views/clusters/clusters/_integrations.html.haml
+++ b/app/views/clusters/clusters/_integrations.html.haml
@@ -9,7 +9,7 @@
= prometheus_form.hidden_field :application_type, value: @prometheus_integration.application_type
.form-group.gl-form-group
- help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Prometheus for metrics.')
- - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "prometheus-cluster-integration"), target: '_blank', rel: 'noopener noreferrer')
+ - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations"), target: '_blank', rel: 'noopener noreferrer')
= prometheus_form.gitlab_ui_checkbox_component :enabled,
s_('ClusterIntegration|Enable Prometheus integration'),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 1fd8e12016c..89f460606cb 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,5 +1,9 @@
= gitlab_ui_form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
= form_errors(@milestone)
+
+ - if @conflict
+ = render 'shared/model_version_conflict', model_name: _('milestone'), link_path: group_milestone_path(@group, @milestone)
+
.form-group
= f.label :title, _("Title")
= f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true
@@ -15,6 +19,8 @@
.clearfix
.error-alert
+ = f.hidden_field :lock_version
+
- if @milestone.new_record?
= f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, class: 'gl-mr-2', pajamas_button: true
= render Pajamas::ButtonComponent.new(href: group_milestones_path(@group)) do
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index 98221125443..397f96d6846 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -2,7 +2,7 @@
This service will be installed on the Mattermost instance at
%strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
%hr
-= form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input' }) do |f|
+= gitlab_ui_form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input' }) do |f|
%h4 Team
%p
= @teams.one? ? 'The team' : 'Select the team'
@@ -42,5 +42,6 @@
%hr
.clearfix
.float-right
- = link_to _('Cancel'), edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg'
- = f.submit 'Install', class: 'gl-button btn btn-confirm btn-lg'
+ = render Pajamas::ButtonComponent.new(href: edit_project_settings_integration_path(@project, @integration)) do
+ = _('Cancel')
+ = f.submit s_('MattermostService|Install'), pajamas_button: true
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 91c161e8602..be6f9ac83dc 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,5 +1,9 @@
= gitlab_ui_form_for [@project, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
= form_errors(@milestone)
+
+ - if @conflict
+ = render 'shared/model_version_conflict', model_name: _('milestone'), link_path: project_milestone_path(@project, @milestone)
+
- if @redirect_path.present?
= f.hidden_field(:redirect_path, name: :redirect_path, id: :redirect_path, value: @redirect_path)
.form-group
@@ -18,6 +22,8 @@
.clearfix
.error-alert
+ = f.hidden_field :lock_version
+
- if @milestone.new_record?
= f.submit _('Create milestone'), data: { qa_selector: 'create_milestone_button' }, class: 'gl-mr-2', pajamas_button: true
= link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-default btn-cancel'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index e21bf7d318b..8a35db357ee 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -7,6 +7,7 @@
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
+= render_if_exists 'shared/promotions/promote_mobile_devops', project: @project
= render partial: 'flash_messages', locals: { project: @project }
= render 'clusters_deprecation_alert'
diff --git a/app/views/shared/_captcha_check.html.haml b/app/views/shared/_captcha_check.html.haml
index a10ae655ea6..b3b2ae0d969 100644
--- a/app/views/shared/_captcha_check.html.haml
+++ b/app/views/shared/_captcha_check.html.haml
@@ -10,7 +10,7 @@
%p
= _("We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed.") % { humanized_resource_name: humanized_resource_name }
-= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
+= gitlab_ui_form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
.recaptcha
-# Create a hidden field for each param of the resource
- params[resource_name].each do |field, value|
@@ -34,4 +34,4 @@
= yield
.row-content-block.footer-block
- = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm'
+ = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, pajamas_button: true
diff --git a/app/views/shared/_model_version_conflict.html.haml b/app/views/shared/_model_version_conflict.html.haml
new file mode 100644
index 00000000000..134dcf8db7f
--- /dev/null
+++ b/app/views/shared/_model_version_conflict.html.haml
@@ -0,0 +1,6 @@
+= render Pajamas::AlertComponent.new(variant: :danger,
+ dismissible: false,
+ alert_options: { class: 'gl-mb-5' }) do |c|
+ = c.body do
+ - link_to_model = link_to(model_name, link_path, target: '_blank', rel: 'noopener noreferrer')
+ = _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs.").html_safe % { model_name: model_name, link_to_model: link_to_model }
diff --git a/app/views/shared/doorkeeper/applications/_form.html.haml b/app/views/shared/doorkeeper/applications/_form.html.haml
index b40e2630011..628a34e1278 100644
--- a/app/views/shared/doorkeeper/applications/_form.html.haml
+++ b/app/views/shared/doorkeeper/applications/_form.html.haml
@@ -21,4 +21,4 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: @application, scopes: @scopes, f: f
.gl-mt-3
- = f.submit _('Save application'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save application'), pajamas_button: true
diff --git a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
index e5ddc055aef..beeb328aedf 100644
--- a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
@@ -3,7 +3,7 @@
.col-lg-3
%p
= s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.')
- = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer"
+ = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
= render Pajamas::CardComponent.new(header_options: { class: 'gl-display-flex gl-align-items-center' }, body_options: { class: 'gl-p-0' }, card_options: { class: 'gl-mb-5 custom-monitored-metrics js-panel-custom-monitored-metrics', data: { active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{integration.active}" } }) do |c|
diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml
index 1c54e4bd1de..7cd4eeee5f8 100644
--- a/app/views/shared/integrations/prometheus/_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_metrics.html.haml
@@ -34,5 +34,5 @@
.flash-notice
.flash-text
= html_escape(s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries.")) % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>".html_safe }
- = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/dashboards/variables.md', anchor: 'query-variables')
+ = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 58d0a39578a..b6bd691213c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -6,14 +6,8 @@
= form_errors(issuable)
- if @conflict
- = render Pajamas::AlertComponent.new(variant: :danger,
- dismissible: false,
- alert_options: { class: 'gl-mb-5' }) do |c|
- = c.body do
- Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
- Please check out
- = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project, issuable]), target: "_blank", rel: 'noopener noreferrer'
- and make sure your changes will not unintentionally remove theirs
+ - model_name = _(issuable.class.model_name.human.downcase)
+ = render 'shared/model_version_conflict', model_name: model_name, link_path: polymorphic_path([@project, issuable])
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 3eb80292a2f..1149f64314e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -165,6 +165,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: container_repository:container_registry_record_data_repair_detail
+ :worker_name: ContainerRegistry::RecordDataRepairDetailWorker
+ :feature_category: :container_registry
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: container_repository:delete_container_repository
:worker_name: DeleteContainerRepositoryWorker
:feature_category: :container_registry
diff --git a/app/workers/container_registry/cleanup_worker.rb b/app/workers/container_registry/cleanup_worker.rb
index a838b97b35d..448a16ad309 100644
--- a/app/workers/container_registry/cleanup_worker.rb
+++ b/app/workers/container_registry/cleanup_worker.rb
@@ -12,14 +12,17 @@ module ContainerRegistry
feature_category :container_registry
STALE_DELETE_THRESHOLD = 30.minutes.freeze
+ STALE_REPAIR_DETAIL_THRESHOLD = 2.hours.freeze
BATCH_SIZE = 200
def perform
log_counts
reset_stale_deletes
+ delete_stale_ongoing_repair_details
enqueue_delete_container_repository_jobs if ContainerRepository.delete_scheduled.exists?
+ enqueue_record_repair_detail_jobs if should_enqueue_record_detail_jobs?
end
private
@@ -33,10 +36,31 @@ module ContainerRegistry
end
end
+ def delete_stale_ongoing_repair_details
+ # Deleting stale ongoing repair details would put the project back to the analysis pool
+ ContainerRegistry::DataRepairDetail
+ .ongoing_since(STALE_REPAIR_DETAIL_THRESHOLD.ago)
+ .each_batch(of: BATCH_SIZE) do |batch| # rubocop:disable Style/SymbolProc
+ batch.delete_all
+ end
+ end
+
def enqueue_delete_container_repository_jobs
ContainerRegistry::DeleteContainerRepositoryWorker.perform_with_capacity
end
+ def enqueue_record_repair_detail_jobs
+ ContainerRegistry::RecordDataRepairDetailWorker.perform_with_capacity
+ end
+
+ def should_enqueue_record_detail_jobs?
+ return false unless Gitlab.com?
+ return false unless Feature.enabled?(:registry_data_repair_worker)
+ return false unless ContainerRegistry::GitlabApiClient.supports_gitlab_api?
+
+ Project.pending_data_repair_analysis.exists?
+ end
+
def log_counts
::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
log_extra_metadata_on_done(
diff --git a/app/workers/container_registry/record_data_repair_detail_worker.rb b/app/workers/container_registry/record_data_repair_detail_worker.rb
new file mode 100644
index 00000000000..f400568a3ef
--- /dev/null
+++ b/app/workers/container_registry/record_data_repair_detail_worker.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module ContainerRegistry
+ class RecordDataRepairDetailWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+ include LimitedCapacity::Worker
+ include Gitlab::Utils::StrongMemoize
+
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
+ queue_namespace :container_repository
+ feature_category :container_registry
+ urgency :low
+ worker_resource_boundary :unknown
+ idempotent!
+
+ MAX_CAPACITY = 2
+ LEASE_TIMEOUT = 1.hour.to_i
+
+ def perform_work
+ return unless Gitlab.com?
+ return unless next_project
+ return if next_project.container_registry_data_repair_detail
+
+ missing_count = 0
+
+ try_obtain_lease do
+ detail = create_data_repair_detail
+
+ GitlabApiClient.each_sub_repositories_with_tag_page(path: next_project.full_path,
+ page_size: 50) do |repositories|
+ next if repositories.empty?
+
+ paths = repositories.map { |repo| ContainerRegistry::Path.new(repo["path"]) }
+ paths, invalid_paths = paths.partition(&:valid?)
+ unless invalid_paths.empty?
+ log_extra_metadata_on_done(
+ :invalid_paths_parsed_in_container_repository_repair,
+ invalid_paths.join(' ,')
+ )
+ end
+
+ found_repositories = next_project.container_repositories.where(name: paths.map(&:repository_name)) # rubocop:disable CodeReuse/ActiveRecord
+
+ missing_count += repositories.count - found_repositories.count
+ end
+ detail.update!(missing_count: missing_count, status: :completed)
+ end
+ rescue StandardError => exception
+ next_project.reset.container_registry_data_repair_detail&.update(status: :failed)
+ Gitlab::ErrorTracking.log_exception(exception, class: self.class.name)
+ end
+
+ def remaining_work_count
+ return 0 unless Gitlab.com?
+ return 0 unless Feature.enabled?(:registry_data_repair_worker)
+ return 0 unless ContainerRegistry::GitlabApiClient.supports_gitlab_api?
+
+ Project.pending_data_repair_analysis.limit(max_running_jobs + 1).count
+ end
+
+ def max_running_jobs
+ MAX_CAPACITY
+ end
+
+ private
+
+ def next_project
+ Project.pending_data_repair_analysis.first
+ end
+ strong_memoize_attr :next_project
+
+ def create_data_repair_detail
+ ContainerRegistry::DataRepairDetail.create!(project: next_project, status: :ongoing)
+ end
+
+ # Used by ExclusiveLeaseGuard
+ def lease_key
+ "container_registry_data_repair_detail_worker:#{next_project.id}"
+ end
+
+ # Used by ExclusiveLeaseGuard
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+ end
+end