Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-30 03:09:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-30 03:09:03 +0300
commitb72218d98e11514569939cf475d3c626fed445d1 (patch)
tree8c69b5590ea68963673f2f4d3a886a5095f3b524
parentba537a9b5cf97308fd9fe099e0d022a9a76b66ad (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml5
-rw-r--r--app/assets/javascripts/attention_requests/components/navigation_popover.vue4
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_token.vue33
-rw-r--r--app/assets/javascripts/clusters_list/constants.js23
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/controllers/jira_connect/events_controller.rb4
-rw-r--r--app/helpers/diff_helper.rb14
-rw-r--r--app/models/bulk_imports/export_status.rb6
-rw-r--r--app/models/ci/namespace_mirror.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb27
-rw-r--r--app/models/programming_language.rb7
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/project_setting.rb9
-rw-r--r--app/models/repository_language.rb4
-rw-r--r--app/models/user.rb20
-rw-r--r--app/services/git/branch_push_service.rb7
-rw-r--r--app/services/projects/apple_target_platform_detector_service.rb58
-rw-r--r--app/services/projects/record_target_platforms_service.rb29
-rw-r--r--app/views/projects/diffs/_line.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml4
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/projects/record_target_platforms_worker.rb55
-rw-r--r--config/feature_flags/development/record_projects_target_platforms.yml (renamed from config/feature_flags/development/jira_connect_installation_update.yml)10
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20220317170122_add_notification_level_to_namespace_root_storage_statistics.rb13
-rw-r--r--db/migrate/20220318120802_add_target_platforms_to_project_setting.rb7
-rw-r--r--db/schema_migrations/202203171701221
-rw-r--r--db/schema_migrations/202203181208021
-rw-r--r--db/structure.sql4
-rw-r--r--lib/gitlab/diff/line.rb17
-rw-r--r--lib/gitlab/diff/parallel_diff.rb2
-rw-r--r--lib/gitlab/diff/rendered/notebook/diff_file.rb9
-rw-r--r--locale/gitlab.pot27
-rw-r--r--spec/controllers/jira_connect/events_controller_spec.rb11
-rw-r--r--spec/features/projects/commits/multi_view_diff_spec.rb79
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js8
-rw-r--r--spec/helpers/diff_helper_spec.rb47
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb6
-rw-r--r--spec/models/bulk_imports/export_status_spec.rb18
-rw-r--r--spec/models/group_spec.rb88
-rw-r--r--spec/models/programming_language_spec.rb18
-rw-r--r--spec/models/project_setting_spec.rb30
-rw-r--r--spec/models/project_spec.rb50
-rw-r--r--spec/models/user_spec.rb22
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/services/git/branch_push_service_spec.rb10
-rw-r--r--spec/services/projects/apple_target_platform_detector_service_spec.rb61
-rw-r--r--spec/services/projects/record_target_platforms_service_spec.rb66
-rw-r--r--spec/workers/projects/record_target_platforms_worker_spec.rb87
51 files changed, 901 insertions, 130 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 146a7067acd..f1c62e01674 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -41,6 +41,7 @@
key:
files:
- GITALY_SERVER_VERSION
+ - lib/gitlab/setup_helper.rb
prefix: "gitaly-binaries-${DEBIAN-VERSION}"
paths:
- tmp/tests/gitaly/_build/bin/
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 8c30fb5447e..e808a0297a6 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -161,6 +161,7 @@
.gitaly-patterns: &gitaly-patterns
- "GITALY_SERVER_VERSION"
+ - "lib/gitlab/setup_helper.rb"
.workhorse-patterns: &workhorse-patterns
- "GITLAB_WORKHORSE_VERSION"
@@ -274,7 +275,9 @@
- "lib/gitlab/markdown_cache/active_record/**/*"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/,jh/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
- - "GITALY_SERVER_VERSION" # Has interactions with background migrations:https://gitlab.com/gitlab-org/gitlab/-/issues/336538
+ # Gitaly has interactions with background migrations: https://gitlab.com/gitlab-org/gitlab/-/issues/336538
+ - "GITALY_SERVER_VERSION"
+ - "lib/gitlab/setup_helper.rb"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
diff --git a/app/assets/javascripts/attention_requests/components/navigation_popover.vue b/app/assets/javascripts/attention_requests/components/navigation_popover.vue
index 1542bc9a7e9..804eda8f321 100644
--- a/app/assets/javascripts/attention_requests/components/navigation_popover.vue
+++ b/app/assets/javascripts/attention_requests/components/navigation_popover.vue
@@ -82,7 +82,9 @@ export default {
return 'bottom';
},
},
- docsPage: helpPagePath('development/code_review.html'),
+ docsPage: helpPagePath('user/project/merge_requests/index.md', {
+ anchor: 'request-attention-to-a-merge-request',
+ }),
};
</script>
diff --git a/app/assets/javascripts/clusters_list/components/agent_token.vue b/app/assets/javascripts/clusters_list/components/agent_token.vue
index eab3fc3ed63..951cf7926b4 100644
--- a/app/assets/javascripts/clusters_list/components/agent_token.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_token.vue
@@ -8,9 +8,6 @@ import { I18N_AGENT_TOKEN } from '../constants';
export default {
i18n: I18N_AGENT_TOKEN,
- basicInstallPath: helpPagePath('user/clusters/agent/install/index', {
- anchor: 'install-the-agent-into-the-cluster',
- }),
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation',
}),
@@ -43,27 +40,7 @@ export default {
<template>
<div>
- <p>
- <strong>{{ $options.i18n.tokenTitle }}</strong>
- </p>
-
- <p>
- <gl-sprintf :message="$options.i18n.tokenBody">
- <template #link="{ content }">
- <gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
-
- <p>
- <gl-alert
- :title="$options.i18n.tokenSingleUseWarningTitle"
- variant="warning"
- :dismissible="false"
- >
- {{ $options.i18n.tokenSingleUseWarningBody }}
- </gl-alert>
- </p>
+ <p class="gl-mb-3">{{ $options.i18n.tokenLabel }}</p>
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
@@ -78,6 +55,14 @@ export default {
</p>
<p>
+ {{ $options.i18n.tokenSubtitle }}
+ </p>
+
+ <gl-alert :dismissible="false" variant="warning" class="gl-mb-5">
+ {{ $options.i18n.tokenSingleUseWarningTitle }}
+ </gl-alert>
+
+ <p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 7bf1ed607b9..c1862842082 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -90,26 +90,20 @@ export const I18N_AGENT_TABLE = {
export const I18N_AGENT_TOKEN = {
copyToken: s__('ClusterAgents|Copy token'),
copyCommand: s__('ClusterAgents|Copy command'),
- tokenTitle: s__('ClusterAgents|Registration token'),
-
- tokenBody: s__(
- `ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}`,
- ),
+ tokenLabel: s__('ClusterAgents|Agent access token:'),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|You cannot see this token again after you close this window.',
),
- tokenSingleUseWarningBody: s__(
- `ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window.`,
- ),
+ tokenSubtitle: s__('ClusterAgents|The agent uses the token to connect with GitLab.'),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
- basicInstallBody: __(
- `Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
+ basicInstallBody: s__(
+ 'ClusterAgents|From a terminal, connect to your cluster and run this command. The token is included.',
),
advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
advancedInstallBody: s__(
- 'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
+ 'ClusterAgents|%{linkStart}View the documentation%{linkEnd} for advanced installation. Ensure you have your access token available.',
),
};
@@ -118,7 +112,7 @@ export const I18N_AGENT_MODAL = {
close: __('Close'),
cancel: __('Cancel'),
- modalTitle: s__('ClusterAgents|Connect a cluster through an agent'),
+ modalTitle: s__('ClusterAgents|Connect a Kubernetes cluster'),
modalBody: s__(
'ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:',
),
@@ -127,11 +121,6 @@ export const I18N_AGENT_MODAL = {
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
learnMoreLink: s__('ClusterAgents|How do I register an agent?'),
- copyToken: s__('ClusterAgents|Copy token'),
- tokenTitle: s__('ClusterAgents|Registration token'),
- tokenBody: s__(
- `ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}`,
- ),
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 215b4b7b2d7..97b6bb5a973 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -385,7 +385,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
dashboardPath,
},
})
- .then((resp) => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard)
+ .then((resp) => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard || undefined)
.then(({ schemaValidationWarnings } = {}) => {
const hasWarnings = schemaValidationWarnings && schemaValidationWarnings.length !== 0;
/**
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index 327192857f6..3c78f63e069 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -7,10 +7,6 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
before_action :verify_asymmetric_atlassian_jwt!
def installed
- unless Feature.enabled?(:jira_connect_installation_update, default_enabled: :yaml)
- return head :ok if current_jira_installation
- end
-
success = current_jira_installation ? update_installation : create_installation
if success
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 100d5c0281c..121bd260928 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -60,6 +60,18 @@ module DiffHelper
html.join.html_safe
end
+ def diff_nomappinginraw_line(line, first_line_num_class, second_line_num_class, content_line_class)
+ css_class = ''
+ css_class = 'old' if line.type == 'old-nomappinginraw'
+ css_class = 'new' if line.type == 'new-nomappinginraw'
+
+ html = [content_tag(:td, '', class: [*first_line_num_class, css_class])]
+ html << content_tag(:td, '', class: [*second_line_num_class, css_class]) if second_line_num_class
+ html << content_tag(:td, diff_line_content(line.rich_text), class: [*content_line_class, 'nomappinginraw', css_class])
+
+ html.join.html_safe
+ end
+
def diff_line_content(line)
if line.blank?
"&nbsp;".html_safe
@@ -74,7 +86,7 @@ module DiffHelper
end
def diff_link_number(line_type, match, text)
- line_type == match || text == 0 ? " " : text
+ line_type == match ? " " : text
end
def parallel_diff_discussions(left, right, diff_file)
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
index cae6aad27da..a9750a76987 100644
--- a/app/models/bulk_imports/export_status.rb
+++ b/app/models/bulk_imports/export_status.rb
@@ -32,10 +32,12 @@ module BulkImports
strong_memoize(:export_status) do
status = fetch_export_status
+ relation_export_status = status&.find { |item| item['relation'] == relation }
+
# Consider empty response as failed export
- raise StandardError, 'Empty export status response' unless status&.present?
+ raise StandardError, 'Empty relation export status' unless relation_export_status&.present?
- status.find { |item| item['relation'] == relation }
+ relation_export_status
end
rescue StandardError => e
{ 'status' => Export::FAILED, 'error' => e.message }
diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb
index d5cbbb96134..b4b9574942e 100644
--- a/app/models/ci/namespace_mirror.rb
+++ b/app/models/ci/namespace_mirror.rb
@@ -11,6 +11,8 @@ module Ci
end
scope :contains_any_of_namespaces, -> (ids) do
+ return none if ids.empty?
+
where('traversal_ids && ARRAY[?]::int[]', ids)
end
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 1963745cf4d..6320e0bc39d 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -49,6 +49,33 @@ module Namespaces
before_commit :sync_traversal_ids, on: [:create], if: -> { sync_traversal_ids? }
end
+ class_methods do
+ # This method looks into a list of namespaces trying to optimise a returned traversal_ids
+ # into a list of shortest prefixes, due to fact that the shortest prefixes include all childrens.
+ # Example:
+ # INPUT: [[4909902], [4909902,51065789], [4909902,51065793], [7135830], [15599674, 1], [15599674, 1, 3], [15599674, 2]]
+ # RESULT: [[4909902], [7135830], [15599674, 1], [15599674, 2]]
+ def shortest_traversal_ids_prefixes
+ raise ArgumentError, 'Feature not supported since the `:use_traversal_ids` is disabled' unless use_traversal_ids?
+
+ prefixes = []
+
+ # The array needs to be sorted (O(nlogn)) to ensure shortest elements are always first
+ # This allows to do O(n) search of shortest prefixes
+ all_traversal_ids = all.order('namespaces.traversal_ids').pluck('namespaces.traversal_ids')
+ last_prefix = [nil]
+
+ all_traversal_ids.each do |traversal_ids|
+ next if last_prefix == traversal_ids[0..(last_prefix.count - 1)]
+
+ last_prefix = traversal_ids
+ prefixes << traversal_ids
+ end
+
+ prefixes
+ end
+ end
+
def sync_traversal_ids?
Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml)
end
diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb
index 375fbe9b5a9..06e3034e56a 100644
--- a/app/models/programming_language.rb
+++ b/app/models/programming_language.rb
@@ -4,9 +4,10 @@ class ProgrammingLanguage < ApplicationRecord
validates :name, presence: true
validates :color, allow_blank: false, color: true
- # Returns all programming languages which match the given name (case
+ # Returns all programming languages which match any of the given names (case
# insensitively).
- scope :with_name_case_insensitive, ->(name) do
- where(arel_table[:name].matches(sanitize_sql_like(name)))
+ scope :with_name_case_insensitive, ->(*names) do
+ sanitized_names = names.map(&method(:sanitize_sql_like))
+ where(arel_table[:name].matches_any(sanitized_names))
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ecbb71806b8..8ffadf39c9c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1989,6 +1989,8 @@ class Project < ApplicationRecord
ProjectCacheWorker.perform_async(self.id, [], [:repository_size])
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(id)
+ enqueue_record_project_target_platforms
+
# The import assigns iid values on its own, e.g. by re-using GitHub ids.
# Flush existing InternalId records for this project for consistency reasons.
# Those records are going to be recreated with the next normal creation
@@ -2848,6 +2850,13 @@ class Project < ApplicationRecord
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self, default_enabled: :yaml)
end
+ def enqueue_record_project_target_platforms
+ return unless Gitlab.com?
+ return unless Feature.enabled?(:record_projects_target_platforms, self, default_enabled: :yaml)
+
+ Projects::RecordTargetPlatformsWorker.perform_async(id)
+ end
+
private
# overridden in EE
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index c3c1025ed79..6cd6eee2616 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
+ ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos).freeze
+
belongs_to :project, inverse_of: :project_setting
enum squash_option: {
@@ -14,6 +16,9 @@ class ProjectSetting < ApplicationRecord
validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :squash_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
+ validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
+
+ validate :validates_mr_default_target_self
default_value_for(:legacy_open_source_license_available) do
Feature.enabled?(:legacy_open_source_license_available, default_enabled: :yaml, type: :ops)
@@ -27,7 +32,9 @@ class ProjectSetting < ApplicationRecord
%w[always never].include?(squash_option)
end
- validate :validates_mr_default_target_self
+ def target_platforms=(val)
+ super(val&.map(&:to_s)&.sort)
+ end
private
diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb
index 2816aa4cc5b..60aaa1f932a 100644
--- a/app/models/repository_language.rb
+++ b/app/models/repository_language.rb
@@ -8,8 +8,8 @@ class RepositoryLanguage < ApplicationRecord
default_scope { includes(:programming_language) } # rubocop:disable Cop/DefaultScope
- scope :with_programming_language, ->(name) do
- joins(:programming_language).merge(ProgrammingLanguage.with_name_case_insensitive(name))
+ scope :with_programming_language, ->(*names) do
+ joins(:programming_language).merge(ProgrammingLanguage.with_name_case_insensitive(*names))
end
validates :project, presence: true
diff --git a/app/models/user.rb b/app/models/user.rb
index b3bdc2c1c42..ae3a9888d0c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2244,9 +2244,23 @@ class User < ApplicationRecord
end
def ci_namespace_mirrors_for_group_members(level)
- Ci::NamespaceMirror.contains_any_of_namespaces(
- group_members.where('access_level >= ?', level).pluck(:source_id)
- )
+ search_members = group_members.where('access_level >= ?', level)
+
+ # This reduces searched prefixes to only shortest ones
+ # to avoid querying descendants since they are already covered
+ # by ancestor namespaces. If the FF is not available fallback to
+ # inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436
+ namespace_ids =
+ if Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ Group.joins(:all_group_members)
+ .merge(search_members)
+ .shortest_traversal_ids_prefixes
+ .map(&:last)
+ else
+ search_members.pluck(:source_id)
+ end
+
+ Ci::NamespaceMirror.contains_any_of_namespaces(namespace_ids)
end
end
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index 13223872e4f..3c27ad56ebb 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -24,6 +24,7 @@ module Git
enqueue_update_mrs
enqueue_detect_repository_languages
+ enqueue_record_project_target_platforms
execute_related_hooks
@@ -53,6 +54,12 @@ module Git
DetectRepositoryLanguagesWorker.perform_async(project.id)
end
+ def enqueue_record_project_target_platforms
+ return unless default_branch?
+
+ project.enqueue_record_project_target_platforms
+ end
+
# Only stop environments if the ref is a branch that is being deleted
def stop_environments
return unless removing_branch?
diff --git a/app/services/projects/apple_target_platform_detector_service.rb b/app/services/projects/apple_target_platform_detector_service.rb
new file mode 100644
index 00000000000..ec4c16a1416
--- /dev/null
+++ b/app/services/projects/apple_target_platform_detector_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class to detect target platforms of a project made for the Apple
+ # Ecosystem.
+ #
+ # This service searches project.pbxproj and *.xcconfig files (contains build
+ # settings) for the string "SDKROOT = <SDK_name>" where SDK_name can be
+ # 'iphoneos', 'macosx', 'appletvos' or 'watchos'. Currently, the service is
+ # intentionally limited (for performance reasons) to detect if a project
+ # targets iOS.
+ #
+ # Ref: https://developer.apple.com/documentation/xcode/build-settings-reference/
+ #
+ # Example usage:
+ # > AppleTargetPlatformDetectorService.new(a_project).execute
+ # => []
+ # > AppleTargetPlatformDetectorService.new(an_ios_project).execute
+ # => [:ios]
+ # > AppleTargetPlatformDetectorService.new(multiplatform_project).execute
+ # => [:ios, :osx, :tvos, :watchos]
+ class AppleTargetPlatformDetectorService < BaseService
+ BUILD_CONFIG_FILENAMES = %w(project.pbxproj *.xcconfig).freeze
+
+ # For the current iteration, we only want to detect when the project targets
+ # iOS. In the future, we can use the same logic to detect projects that
+ # target OSX, TvOS, and WatchOS platforms with SDK names 'macosx', 'appletvos',
+ # and 'watchos', respectively.
+ PLATFORM_SDK_NAMES = { ios: 'iphoneos' }.freeze
+
+ def execute
+ detect_platforms
+ end
+
+ private
+
+ def file_finder
+ @file_finder ||= ::Gitlab::FileFinder.new(project, project.default_branch)
+ end
+
+ def detect_platforms
+ # Return array of SDK names for which "SDKROOT = <sdk_name>" setting
+ # definition can be found in either project.pbxproj or *.xcconfig files.
+ PLATFORM_SDK_NAMES.select do |_, sdk|
+ config_files_containing_sdk_setting(sdk).present?
+ end.keys
+ end
+
+ # Return array of project.pbxproj and/or *.xcconfig files
+ # (Gitlab::Search::FoundBlob) that contain the setting definition string
+ # "SDKROOT = <sdk_name>"
+ def config_files_containing_sdk_setting(sdk)
+ BUILD_CONFIG_FILENAMES.map do |filename|
+ file_finder.find("SDKROOT = #{sdk} filename:#{filename}")
+ end.flatten
+ end
+ end
+end
diff --git a/app/services/projects/record_target_platforms_service.rb b/app/services/projects/record_target_platforms_service.rb
new file mode 100644
index 00000000000..224e16f53b3
--- /dev/null
+++ b/app/services/projects/record_target_platforms_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Projects
+ class RecordTargetPlatformsService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ record_target_platforms
+ end
+
+ private
+
+ def target_platforms
+ strong_memoize(:target_platforms) do
+ AppleTargetPlatformDetectorService.new(project).execute
+ end
+ end
+
+ def record_target_platforms
+ return unless target_platforms.present?
+
+ setting = ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord
+ setting.target_platforms = target_platforms
+ setting.save
+
+ setting.target_platforms
+ end
+ end
+end
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index a5d3328b439..dd5114e3cec 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -10,6 +10,8 @@
- case line.type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
+ - when 'old-nomappinginraw', 'new-nomappinginraw', 'unchanged-nomappinginraw'
+ = diff_nomappinginraw_line line, %w[old_line diff-line-num], %w[new_line diff-line-num], %w[line_content]
- when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index ebe3aad064a..03fe3e6edf5 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -11,6 +11,8 @@
- case left.type
- when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
+ - when 'old-nomappinginraw', 'new-nomappinginraw', 'unchanged-nomappinginraw'
+ = diff_nomappinginraw_line left, %w[old_line diff-line-num], nil, %w[line_content parallel left-side]
- when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.line_content.match.left-side= left.text
@@ -29,6 +31,8 @@
- case right.type
- when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
+ - when 'old-nomappinginraw', 'new-nomappinginraw', 'unchanged-nomappinginraw'
+ = diff_nomappinginraw_line right, %w[new_line diff-line-num], nil, %w[line_content parallel right-side]
- when 'old-nonewline', 'new-nonewline'
%td.new_line.diff-line-num
%td.line_content.match.right-side= right.text
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 6e7e0244721..2cd215c5518 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -12,6 +12,8 @@
- case line.type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
+ - when 'old-nomappinginraw', 'new-nomappinginraw', 'unchanged-nomappinginraw'
+ = diff_nomappinginraw_line line, %w[old_line diff-line-num], %w[new_line diff-line-num], %w[line_content]
- when 'old-nonewline', 'new-nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 426a4901c45..2f450998455 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2812,6 +2812,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: projects_record_target_platforms
+ :worker_name: Projects::RecordTargetPlatformsWorker
+ :feature_category: :experimentation_activation
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_refresh_build_artifacts_size_statistics
:worker_name: Projects::RefreshBuildArtifactsSizeStatisticsWorker
:feature_category: :build_artifacts
diff --git a/app/workers/projects/record_target_platforms_worker.rb b/app/workers/projects/record_target_platforms_worker.rb
new file mode 100644
index 00000000000..5b1f85ecca0
--- /dev/null
+++ b/app/workers/projects/record_target_platforms_worker.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Projects
+ class RecordTargetPlatformsWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour.to_i
+ APPLE_PLATFORM_LANGUAGES = %w(swift objective-c).freeze
+
+ feature_category :experimentation_activation
+ data_consistency :always
+ deduplicate :until_executed
+ urgency :low
+ idempotent!
+
+ def perform(project_id)
+ @project = Project.find_by_id(project_id)
+
+ return unless project
+ return unless uses_apple_platform_languages?
+
+ try_obtain_lease do
+ @target_platforms = Projects::RecordTargetPlatformsService.new(project).execute
+ log_target_platforms_metadata
+ end
+ end
+
+ private
+
+ attr_reader :target_platforms, :project
+
+ def uses_apple_platform_languages?
+ project.repository_languages.with_programming_language(*APPLE_PLATFORM_LANGUAGES).present?
+ end
+
+ def log_target_platforms_metadata
+ return unless target_platforms.present?
+
+ log_extra_metadata_on_done(:target_platforms, target_platforms)
+ end
+
+ def lease_key
+ @lease_key ||= "#{self.class.name.underscore}:#{project.id}"
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_release?
+ false
+ end
+ end
+end
diff --git a/config/feature_flags/development/jira_connect_installation_update.yml b/config/feature_flags/development/record_projects_target_platforms.yml
index a92a7dafc14..6faeab3afe4 100644
--- a/config/feature_flags/development/jira_connect_installation_update.yml
+++ b/config/feature_flags/development/record_projects_target_platforms.yml
@@ -1,8 +1,8 @@
---
-name: jira_connect_installation_update
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83038
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356083
-milestone: '14.9'
+name: record_projects_target_platforms
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80361
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354286
+milestone: '14.10'
type: development
-group: group::integrations
+group: group::activation
default_enabled: false
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index dab2ced37f1..7ec142344ee 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -361,6 +361,8 @@
- 1
- - projects_process_sync_events
- 1
+- - projects_record_target_platforms
+ - 1
- - projects_refresh_build_artifacts_size_statistics
- 1
- - projects_schedule_bulk_repository_shard_moves
diff --git a/db/migrate/20220317170122_add_notification_level_to_namespace_root_storage_statistics.rb b/db/migrate/20220317170122_add_notification_level_to_namespace_root_storage_statistics.rb
new file mode 100644
index 00000000000..3c0b41e0b42
--- /dev/null
+++ b/db/migrate/20220317170122_add_notification_level_to_namespace_root_storage_statistics.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddNotificationLevelToNamespaceRootStorageStatistics < Gitlab::Database::Migration[1.0]
+ enable_lock_retries!
+
+ def up
+ add_column :namespace_root_storage_statistics, :notification_level, :integer, limit: 2, default: 100, null: false
+ end
+
+ def down
+ remove_column :namespace_root_storage_statistics, :notification_level
+ end
+end
diff --git a/db/migrate/20220318120802_add_target_platforms_to_project_setting.rb b/db/migrate/20220318120802_add_target_platforms_to_project_setting.rb
new file mode 100644
index 00000000000..0e8f206e56d
--- /dev/null
+++ b/db/migrate/20220318120802_add_target_platforms_to_project_setting.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTargetPlatformsToProjectSetting < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :project_settings, :target_platforms, :string, array: true, default: [], null: false, if_not_exists: true
+ end
+end
diff --git a/db/schema_migrations/20220317170122 b/db/schema_migrations/20220317170122
new file mode 100644
index 00000000000..9b75fa2042a
--- /dev/null
+++ b/db/schema_migrations/20220317170122
@@ -0,0 +1 @@
+0bd78ce207cca13b5c4557ace87c135972ed69cd05ee8c6fcc60a9c060ba8b5f \ No newline at end of file
diff --git a/db/schema_migrations/20220318120802 b/db/schema_migrations/20220318120802
new file mode 100644
index 00000000000..a306b3cdcd7
--- /dev/null
+++ b/db/schema_migrations/20220318120802
@@ -0,0 +1 @@
+19f25b2f373e7c2799812661baca1902c9c74df67f7a5e88116862fb078a5957 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e0dcba3107a..c3f56604358 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17331,7 +17331,8 @@ CREATE TABLE namespace_root_storage_statistics (
snippets_size bigint DEFAULT 0 NOT NULL,
pipeline_artifacts_size bigint DEFAULT 0 NOT NULL,
uploads_size bigint DEFAULT 0 NOT NULL,
- dependency_proxy_size bigint DEFAULT 0 NOT NULL
+ dependency_proxy_size bigint DEFAULT 0 NOT NULL,
+ notification_level smallint DEFAULT 100 NOT NULL
);
CREATE TABLE namespace_settings (
@@ -19371,6 +19372,7 @@ CREATE TABLE project_settings (
has_shimo boolean DEFAULT false NOT NULL,
squash_commit_template text,
legacy_open_source_license_available boolean DEFAULT true NOT NULL,
+ target_platforms character varying[] DEFAULT '{}'::character varying[] NOT NULL,
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)),
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index c2b834c71b5..316a0d2815a 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -9,8 +9,8 @@ module Gitlab
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
attr_reader :marker_ranges
- attr_writer :text, :rich_text, :discussable
- attr_accessor :index, :type, :old_pos, :new_pos, :line_code
+ attr_writer :text, :rich_text
+ attr_accessor :index, :old_pos, :new_pos, :line_code, :type
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
@text = text
@@ -24,9 +24,7 @@ module Gitlab
# When line code is not provided from cache store we build it
# using the parent_file(Diff::File or Conflict::File).
@line_code = line_code || calculate_line_code
-
@marker_ranges = []
- @discussable = true
end
def self.init_from_hash(hash)
@@ -81,23 +79,28 @@ module Gitlab
end
def added?
- %w[new new-nonewline].include?(type)
+ %w[new new-nonewline new-nomappinginraw].include?(type)
end
def removed?
- %w[old old-nonewline].include?(type)
+ %w[old old-nonewline old-nomappinginraw].include?(type)
end
def meta?
%w[match new-nonewline old-nonewline].include?(type)
end
+ def has_mapping_in_raw?
+ # Used for rendered diff, when the displayed line doesn't have a matching line in the raw diff
+ !type&.ends_with?('nomappinginraw')
+ end
+
def match?
type == :match
end
def discussable?
- @discussable && !meta?
+ has_mapping_in_raw? && !meta?
end
def suggestible?
diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb
index 77b65fea726..cbfc20d3d62 100644
--- a/lib/gitlab/diff/parallel_diff.rb
+++ b/lib/gitlab/diff/parallel_diff.rb
@@ -44,7 +44,7 @@ module Gitlab
free_right_index = nil
i += 1
end
- elsif line.meta? || line.unchanged?
+ elsif line.meta? || line.unchanged? || !line.has_mapping_in_raw?
# line in the right panel is the same as in the left one
lines << {
left: line,
diff --git a/lib/gitlab/diff/rendered/notebook/diff_file.rb b/lib/gitlab/diff/rendered/notebook/diff_file.rb
index e700e730f20..fb70f80c21b 100644
--- a/lib/gitlab/diff/rendered/notebook/diff_file.rb
+++ b/lib/gitlab/diff/rendered/notebook/diff_file.rb
@@ -87,10 +87,7 @@ module Gitlab
line.new_pos = removal_line_maps[line.old_pos] if line.new_pos == 0 && line.old_pos != 0
# Lines that do not appear on the original diff should not be commentable
-
- unless addition_line_maps[line.new_pos] || removal_line_maps[line.old_pos]
- line.discussable = false
- end
+ line.type = "#{line.type || 'unchanged'}-nomappinginraw" unless addition_line_maps[line.new_pos] || removal_line_maps[line.old_pos]
line.line_code = line_code(line)
line
@@ -113,8 +110,8 @@ module Gitlab
additions = {}
source_diff.highlighted_diff_lines.each do |line|
- removals[line.old_pos] = line.new_pos
- additions[line.new_pos] = line.old_pos
+ removals[line.old_pos] = line.new_pos unless source_diff.new_file?
+ additions[line.new_pos] = line.old_pos unless source_diff.deleted_file?
end
[removals, additions]
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 12e17901aea..ee11aa6028a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7807,6 +7807,9 @@ msgstr ""
msgid "Cluster type must be specified for Stages::ClusterEndpointInserter"
msgstr ""
+msgid "ClusterAgents|%{linkStart}View the documentation%{linkEnd} for advanced installation. Ensure you have your access token available."
+msgstr ""
+
msgid "ClusterAgents|%{name} successfully deleted"
msgstr ""
@@ -7846,6 +7849,9 @@ msgstr ""
msgid "ClusterAgents|Agent %{strongStart}disconnected%{strongEnd}"
msgstr ""
+msgid "ClusterAgents|Agent access token:"
+msgstr ""
+
msgid "ClusterAgents|Agent might not be connected to GitLab"
msgstr ""
@@ -7885,6 +7891,9 @@ msgstr ""
msgid "ClusterAgents|Configuration"
msgstr ""
+msgid "ClusterAgents|Connect a Kubernetes cluster"
+msgstr ""
+
msgid "ClusterAgents|Connect a cluster"
msgstr ""
@@ -7897,9 +7906,6 @@ msgstr ""
msgid "ClusterAgents|Connect a cluster (deprecated)"
msgstr ""
-msgid "ClusterAgents|Connect a cluster through an agent"
-msgstr ""
-
msgid "ClusterAgents|Connect existing cluster"
msgstr ""
@@ -7969,7 +7975,7 @@ msgstr ""
msgid "ClusterAgents|Failed to register an agent"
msgstr ""
-msgid "ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}."
+msgid "ClusterAgents|From a terminal, connect to your cluster and run this command. The token is included."
msgstr ""
msgid "ClusterAgents|GitLab Agent"
@@ -8032,9 +8038,6 @@ msgstr ""
msgid "ClusterAgents|Registering agent"
msgstr ""
-msgid "ClusterAgents|Registration token"
-msgstr ""
-
msgid "ClusterAgents|Requires a Maintainer or greater role to delete agents"
msgstr ""
@@ -8065,13 +8068,10 @@ msgstr ""
msgid "ClusterAgents|The agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgstr ""
-msgid "ClusterAgents|The agent version do not match each other across your cluster's pods. This can happen when a new agent version was just deployed and Kubernetes is shutting down the old pods."
-msgstr ""
-
-msgid "ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window."
+msgid "ClusterAgents|The agent uses the token to connect with GitLab."
msgstr ""
-msgid "ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}"
+msgid "ClusterAgents|The agent version do not match each other across your cluster's pods. This can happen when a new agent version was just deployed and Kubernetes is shutting down the old pods."
msgstr ""
msgid "ClusterAgents|There's no activity from the past day"
@@ -26150,9 +26150,6 @@ msgstr ""
msgid "Open Selection"
msgstr ""
-msgid "Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command."
-msgstr ""
-
msgid "Open errors"
msgstr ""
diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb
index 2129b24b2fb..5e90ceb0f9c 100644
--- a/spec/controllers/jira_connect/events_controller_spec.rb
+++ b/spec/controllers/jira_connect/events_controller_spec.rb
@@ -114,17 +114,6 @@ RSpec.describe JiraConnect::EventsController do
base_url: base_url
)
end
-
- context 'when the `jira_connect_installation_update` feature flag is disabled' do
- before do
- stub_feature_flags(jira_connect_installation_update: false)
- end
-
- it 'does not update the installation', :aggregate_failures do
- expect { subject }.not_to change { installation.reload.attributes }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
end
context 'when the new base_url is invalid' do
diff --git a/spec/features/projects/commits/multi_view_diff_spec.rb b/spec/features/projects/commits/multi_view_diff_spec.rb
index ecdd398c739..009dd05c6d1 100644
--- a/spec/features/projects/commits/multi_view_diff_spec.rb
+++ b/spec/features/projects/commits/multi_view_diff_spec.rb
@@ -27,17 +27,11 @@ RSpec.describe 'Multiple view Diffs', :js do
context 'when :rendered_diffs_viewer is off' do
context 'and diff does not have ipynb' do
- include_examples "no multiple viewers", 'ddd0f15ae83993f5cb66a927a28673882e99100b'
+ it_behaves_like "no multiple viewers", 'ddd0f15ae83993f5cb66a927a28673882e99100b'
end
context 'and diff has ipynb' do
- include_examples "no multiple viewers", '5d6ed1503801ca9dc28e95eeb85a7cf863527aee'
-
- it 'shows the transformed diff' do
- diff = page.find('.diff-file, .file-holder', match: :first)
-
- expect(diff['innerHTML']).to include('%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:')
- end
+ it_behaves_like "no multiple viewers", '5d6ed1503801ca9dc28e95eeb85a7cf863527aee'
end
end
@@ -45,14 +39,28 @@ RSpec.describe 'Multiple view Diffs', :js do
let(:feature_flag_on) { true }
context 'and diff does not include ipynb' do
- include_examples "no multiple viewers", 'ddd0f15ae83993f5cb66a927a28673882e99100b'
- end
+ it_behaves_like "no multiple viewers", 'ddd0f15ae83993f5cb66a927a28673882e99100b'
- context 'and opening a diff with ipynb' do
- context 'but the changes are not renderable' do
- include_examples "no multiple viewers", 'a867a602d2220e5891b310c07d174fbe12122830'
+ context 'and in inline diff' do
+ let(:ref) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ it 'does not change display for non-ipynb' do
+ expect(page).to have_selector line_with_content('new', 1)
+ end
end
+ context 'and in parallel diff' do
+ let(:ref) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ it 'does not change display for non-ipynb' do
+ page.find('#parallel-diff-btn').click
+
+ expect(page).to have_selector line_with_content('new', 1)
+ end
+ end
+ end
+
+ context 'and opening a diff with ipynb' do
it 'loads the rendered diff as hidden' do
diff = page.find('.diff-file, .file-holder', match: :first)
@@ -76,10 +84,55 @@ RSpec.describe 'Multiple view Diffs', :js do
expect(classes_for_element(diff, 'toHideBtn')).not_to include('selected')
expect(classes_for_element(diff, 'toShowBtn')).to include('selected')
end
+
+ it 'transforms the diff' do
+ diff = page.find('.diff-file, .file-holder', match: :first)
+
+ expect(diff['innerHTML']).to include('%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:')
+ end
+
+ context 'on parallel view' do
+ before do
+ page.find('#parallel-diff-btn').click
+ end
+
+ it 'lines without mapping cannot receive comments' do
+ expect(page).not_to have_selector('td.line_content.nomappinginraw ~ td.diff-line-num > .add-diff-note')
+ expect(page).to have_selector('td.line_content:not(.nomappinginraw) ~ td.diff-line-num > .add-diff-note')
+ end
+
+ it 'lines numbers without mapping are empty' do
+ expect(page).not_to have_selector('td.nomappinginraw + td.diff-line-num')
+ expect(page).to have_selector('td.nomappinginraw + td.diff-line-num', visible: false)
+ end
+
+ it 'transforms the diff' do
+ diff = page.find('.diff-file, .file-holder', match: :first)
+
+ expect(diff['innerHTML']).to include('%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:')
+ end
+ end
+
+ context 'on inline view' do
+ it 'lines without mapping cannot receive comments' do
+ expect(page).not_to have_selector('tr.line_holder[class$="nomappinginraw"] > td.diff-line-num > .add-diff-note')
+ expect(page).to have_selector('tr.line_holder:not([class$="nomappinginraw"]) > td.diff-line-num > .add-diff-note')
+ end
+
+ it 'lines numbers without mapping are empty' do
+ elements = page.all('tr.line_holder[class$="nomappinginraw"] > td.diff-line-num').map { |e| e.text(:all) }
+
+ expect(elements).to all(be == "")
+ end
+ end
end
end
def classes_for_element(node, data_diff_entity, visible: true)
node.find("[data-diff-toggle-entity=\"#{data_diff_entity}\"]", visible: visible)[:class]
end
+
+ def line_with_content(old_or_new, line_number)
+ "td.#{old_or_new}_line.diff-line-num[data-linenumber=\"#{line_number}\"] > a[data-linenumber=\"#{line_number}\"]"
+ end
end
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index a80c8ffaad4..2c47f12ace1 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -53,7 +53,7 @@ describe('InstallAgentModal', () => {
});
it('shows agent token as an input value', () => {
- expect(findInput().props('value')).toBe('agent-token');
+ expect(findInput().props('value')).toBe(agentToken);
});
it('renders a copy button', () => {
@@ -65,12 +65,12 @@ describe('InstallAgentModal', () => {
});
it('shows warning alert', () => {
- expect(findAlert().props('title')).toBe(I18N_AGENT_TOKEN.tokenSingleUseWarningTitle);
+ expect(findAlert().text()).toBe(I18N_AGENT_TOKEN.tokenSingleUseWarningTitle);
});
it('shows code block with agent installation command', () => {
- expect(findCodeBlock().props('code')).toContain('--agent-token=agent-token');
- expect(findCodeBlock().props('code')).toContain('--kas-address=kas.example.com');
+ expect(findCodeBlock().props('code')).toContain(`--agent-token=${agentToken}`);
+ expect(findCodeBlock().props('code')).toContain(`--kas-address=${kasAddress}`);
});
});
});
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 29708f10de4..ccc2db00236 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -290,6 +290,53 @@ RSpec.describe DiffHelper do
end
end
+ describe "#diff_nomappinginraw_line" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:line) { double("line") }
+ let(:line_type) { 'line_type' }
+
+ before do
+ allow(line).to receive(:rich_text).and_return('line_text')
+ allow(line).to receive(:type).and_return(line_type)
+ end
+
+ it 'generates only single line num' do
+ output = diff_nomappinginraw_line(line, ['line_num_1'], nil, ['line_content'])
+
+ expect(output).to be_html_safe
+ expect(output).to have_css 'td:nth-child(1).line_num_1'
+ expect(output).to have_css 'td:nth-child(2).line_content', text: 'line_text'
+ expect(output).not_to have_css 'td:nth-child(3)'
+ end
+
+ it 'generates only both line nums' do
+ output = diff_nomappinginraw_line(line, ['line_num_1'], ['line_num_2'], ['line_content'])
+
+ expect(output).to be_html_safe
+ expect(output).to have_css 'td:nth-child(1).line_num_1'
+ expect(output).to have_css 'td:nth-child(2).line_num_2'
+ expect(output).to have_css 'td:nth-child(3).line_content', text: 'line_text'
+ end
+
+ where(:line_type, :added_class) do
+ 'old-nomappinginraw' | '.old'
+ 'new-nomappinginraw' | '.new'
+ 'unchanged-nomappinginraw' | ''
+ end
+
+ with_them do
+ it "appends the correct class" do
+ output = diff_nomappinginraw_line(line, ['line_num_1'], ['line_num_2'], ['line_content'])
+
+ expect(output).to be_html_safe
+ expect(output).to have_css 'td:nth-child(1).line_num_1' + added_class
+ expect(output).to have_css 'td:nth-child(2).line_num_2' + added_class
+ expect(output).to have_css 'td:nth-child(3).line_content' + added_class, text: 'line_text'
+ end
+ end
+ end
+
describe '#render_overflow_warning?' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index f2212ec9b09..52ae05af087 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -66,6 +66,12 @@ RSpec.describe Gitlab::Diff::File do
it 'does not have renderable viewer' do
expect(diff_file.has_renderable?).to be_falsey
end
+
+ it 'does not create a Notebook DiffFile' do
+ expect(diff_file.rendered).to be_nil
+
+ expect(::Gitlab::Diff::Rendered::Notebook::DiffFile).not_to receive(:new)
+ end
end
end
diff --git a/spec/models/bulk_imports/export_status_spec.rb b/spec/models/bulk_imports/export_status_spec.rb
index f945ad12244..79ed6b39358 100644
--- a/spec/models/bulk_imports/export_status_spec.rb
+++ b/spec/models/bulk_imports/export_status_spec.rb
@@ -13,6 +13,10 @@ RSpec.describe BulkImports::ExportStatus do
double(parsed_response: [{ 'relation' => 'labels', 'status' => status, 'error' => 'error!' }])
end
+ let(:invalid_response_double) do
+ double(parsed_response: [{ 'relation' => 'not_a_real_relation', 'status' => status, 'error' => 'error!' }])
+ end
+
subject { described_class.new(tracker, relation) }
before do
@@ -36,6 +40,18 @@ RSpec.describe BulkImports::ExportStatus do
it 'returns false' do
expect(subject.started?).to eq(false)
end
+
+ context 'when returned relation is invalid' do
+ before do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client).to receive(:get).and_return(invalid_response_double)
+ end
+ end
+
+ it 'returns false' do
+ expect(subject.started?).to eq(false)
+ end
+ end
end
end
@@ -63,7 +79,7 @@ RSpec.describe BulkImports::ExportStatus do
it 'returns true' do
expect(subject.failed?).to eq(true)
- expect(subject.error).to eq('Empty export status response')
+ expect(subject.error).to eq('Empty relation export status')
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c04ea498328..d2a29228df3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -535,6 +535,10 @@ RSpec.describe Group do
describe '#ancestors_upto' do
it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" }
end
+
+ describe '.shortest_traversal_ids_prefixes' do
+ it { expect { described_class.shortest_traversal_ids_prefixes }.to raise_error /Feature not supported since the `:use_traversal_ids` is disabled/ }
+ end
end
context 'linear' do
@@ -576,6 +580,90 @@ RSpec.describe Group do
it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" }
end
+ describe '.shortest_traversal_ids_prefixes' do
+ subject { filter.shortest_traversal_ids_prefixes }
+
+ context 'for many top-level namespaces' do
+ let!(:top_level_groups) { create_list(:group, 4) }
+
+ context 'when querying all groups' do
+ let(:filter) { described_class.id_in(top_level_groups) }
+
+ it "returns all traversal_ids" do
+ is_expected.to contain_exactly(
+ *top_level_groups.map { |group| [group.id] }
+ )
+ end
+ end
+
+ context 'when querying selected groups' do
+ let(:filter) { described_class.id_in(top_level_groups.first) }
+
+ it "returns only a selected traversal_ids" do
+ is_expected.to contain_exactly([top_level_groups.first.id])
+ end
+ end
+ end
+
+ context 'for namespace hierarchy' do
+ let!(:group_a) { create(:group) }
+ let!(:group_a_sub_1) { create(:group, parent: group_a) }
+ let!(:group_a_sub_2) { create(:group, parent: group_a) }
+ let!(:group_b) { create(:group) }
+ let!(:group_b_sub_1) { create(:group, parent: group_b) }
+ let!(:group_c) { create(:group) }
+
+ context 'when querying all groups' do
+ let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_a_sub_2, group_b, group_b_sub_1, group_c]) }
+
+ it 'returns only shortest prefixes of top-level groups' do
+ is_expected.to contain_exactly(
+ [group_a.id],
+ [group_b.id],
+ [group_c.id]
+ )
+ end
+ end
+
+ context 'when sub-group is reparented' do
+ let(:filter) { described_class.id_in([group_b_sub_1, group_c]) }
+
+ before do
+ group_b_sub_1.update!(parent: group_c)
+ end
+
+ it 'returns a proper shortest prefix of a new group' do
+ is_expected.to contain_exactly(
+ [group_c.id]
+ )
+ end
+ end
+
+ context 'when querying sub-groups' do
+ let(:filter) { described_class.id_in([group_a_sub_1, group_b_sub_1, group_c]) }
+
+ it 'returns sub-groups as they are shortest prefixes' do
+ is_expected.to contain_exactly(
+ [group_a.id, group_a_sub_1.id],
+ [group_b.id, group_b_sub_1.id],
+ [group_c.id]
+ )
+ end
+ end
+
+ context 'when querying group and sub-group of this group' do
+ let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_c]) }
+
+ it 'returns parent groups as this contains all sub-groups' do
+ is_expected.to contain_exactly(
+ [group_a.id],
+ [group_c.id]
+ )
+ end
+ end
+ end
+ end
+
context 'when project namespace exists in the group' do
let!(:project) { create(:project, group: group) }
let!(:project_namespace) { project.project_namespace }
diff --git a/spec/models/programming_language_spec.rb b/spec/models/programming_language_spec.rb
index f2201eabd1c..b202c10e30b 100644
--- a/spec/models/programming_language_spec.rb
+++ b/spec/models/programming_language_spec.rb
@@ -10,4 +10,22 @@ RSpec.describe ProgrammingLanguage do
it { is_expected.to allow_value("#000000").for(:color) }
it { is_expected.not_to allow_value("000000").for(:color) }
it { is_expected.not_to allow_value("#0z0000").for(:color) }
+
+ describe '.with_name_case_insensitive scope' do
+ let_it_be(:ruby) { create(:programming_language, name: 'Ruby') }
+ let_it_be(:python) { create(:programming_language, name: 'Python') }
+ let_it_be(:swift) { create(:programming_language, name: 'Swift') }
+
+ it 'accepts a single name parameter' do
+ expect(described_class.with_name_case_insensitive('swift')).to(
+ contain_exactly(swift)
+ )
+ end
+
+ it 'accepts multiple names' do
+ expect(described_class.with_name_case_insensitive('ruby', 'python')).to(
+ contain_exactly(ruby, python)
+ )
+ end
+ end
end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 5572304d666..d03eb3c8bfe 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -4,4 +4,34 @@ require 'spec_helper'
RSpec.describe ProjectSetting, type: :model do
it { is_expected.to belong_to(:project) }
+
+ describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:target_platforms) }
+ it { is_expected.to allow_value([]).for(:target_platforms) }
+
+ it 'allows any combination of the allowed target platforms' do
+ valid_target_platform_combinations.each do |target_platforms|
+ expect(subject).to allow_value(target_platforms).for(:target_platforms)
+ end
+ end
+
+ [nil, 'not_allowed', :invalid].each do |invalid_value|
+ it { is_expected.not_to allow_value([invalid_value]).for(:target_platforms) }
+ end
+ end
+
+ describe 'target_platforms=' do
+ it 'stringifies and sorts' do
+ project_setting = build(:project_setting, target_platforms: [:watchos, :ios])
+ expect(project_setting.target_platforms).to eq %w(ios watchos)
+ end
+ end
+
+ def valid_target_platform_combinations
+ target_platforms = described_class::ALLOWED_TARGET_PLATFORMS
+
+ 0.upto(target_platforms.size).flat_map do |n|
+ target_platforms.permutation(n).to_a
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 622eb0614bb..8ed9c9051e1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5622,6 +5622,18 @@ RSpec.describe Project, factory_default: :keep do
expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
end
+
+ describe 'project target platforms detection' do
+ before do
+ create(:import_state, :started, project: project)
+ end
+
+ it 'calls enqueue_record_project_target_platforms' do
+ expect(project).to receive(:enqueue_record_project_target_platforms)
+
+ project.after_import
+ end
+ end
end
describe '#update_project_counter_caches' do
@@ -8091,6 +8103,44 @@ RSpec.describe Project, factory_default: :keep do
it_behaves_like 'blocks unsafe serialization'
end
+ describe '#enqueue_record_project_target_platforms' do
+ let_it_be(:project) { create(:project) }
+
+ let(:com) { true }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(com)
+ end
+
+ it 'enqueues a Projects::RecordTargetPlatformsWorker' do
+ expect(Projects::RecordTargetPlatformsWorker).to receive(:perform_async).with(project.id)
+
+ project.enqueue_record_project_target_platforms
+ end
+
+ shared_examples 'does not enqueue a Projects::RecordTargetPlatformsWorker' do
+ it 'does not enqueue a Projects::RecordTargetPlatformsWorker' do
+ expect(Projects::RecordTargetPlatformsWorker).not_to receive(:perform_async)
+
+ project.enqueue_record_project_target_platforms
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(record_projects_target_platforms: false)
+ end
+
+ it_behaves_like 'does not enqueue a Projects::RecordTargetPlatformsWorker'
+ end
+
+ context 'when not in gitlab.com' do
+ let(:com) { false }
+
+ it_behaves_like 'does not enqueue a Projects::RecordTargetPlatformsWorker'
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b16a76211eb..e9657de6ed1 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4251,16 +4251,26 @@ RSpec.describe User do
end
end
- it_behaves_like '#ci_owned_runners'
+ describe '#ci_owned_runners' do
+ it_behaves_like '#ci_owned_runners'
- context 'when FF ci_owned_runners_cross_joins_fix is disabled' do
- before do
- skip_if_multiple_databases_are_setup
+ context 'when FF use_traversal_ids is disabled fallbacks to inefficient implementation' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
- stub_feature_flags(ci_owned_runners_cross_joins_fix: false)
+ it_behaves_like '#ci_owned_runners'
end
- it_behaves_like '#ci_owned_runners'
+ context 'when FF ci_owned_runners_cross_joins_fix is disabled' do
+ before do
+ skip_if_multiple_databases_are_setup
+
+ stub_feature_flags(ci_owned_runners_cross_joins_fix: false)
+ end
+
+ it_behaves_like '#ci_owned_runners'
+ end
end
describe '#projects_with_reporter_access_limited_to' do
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index e55f5820b0f..fbcaa404edb 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -148,6 +148,7 @@ project_setting:
- updated_at
- cve_id_request_enabled
- mr_default_target_self
+ - target_platforms
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index 5a637b0956b..57c130f76a4 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -721,4 +721,14 @@ RSpec.describe Git::BranchPushService, services: true do
it_behaves_like 'does not enqueue Jira sync worker'
end
end
+
+ describe 'project target platforms detection' do
+ subject(:execute) { execute_service(project, user, oldrev: blankrev, newrev: newrev, ref: ref) }
+
+ it 'calls enqueue_record_project_target_platforms on the project' do
+ expect(project).to receive(:enqueue_record_project_target_platforms)
+
+ execute
+ end
+ end
end
diff --git a/spec/services/projects/apple_target_platform_detector_service_spec.rb b/spec/services/projects/apple_target_platform_detector_service_spec.rb
new file mode 100644
index 00000000000..6391161824c
--- /dev/null
+++ b/spec/services/projects/apple_target_platform_detector_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::AppleTargetPlatformDetectorService do
+ let_it_be(:project) { build(:project) }
+
+ subject { described_class.new(project).execute }
+
+ context 'when project is not an xcode project' do
+ before do
+ allow(Gitlab::FileFinder).to receive(:new) { instance_double(Gitlab::FileFinder, find: []) }
+ end
+
+ it 'returns an empty array' do
+ is_expected.to match_array []
+ end
+ end
+
+ context 'when project is an xcode project' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:finder) { instance_double(Gitlab::FileFinder) }
+
+ before do
+ allow(Gitlab::FileFinder).to receive(:new) { finder }
+ end
+
+ def search_query(sdk, filename)
+ "SDKROOT = #{sdk} filename:#{filename}"
+ end
+
+ context 'when setting string is found' do
+ where(:sdk, :filename, :result) do
+ 'iphoneos' | 'project.pbxproj' | [:ios]
+ 'iphoneos' | '*.xcconfig' | [:ios]
+ end
+
+ with_them do
+ before do
+ allow(finder).to receive(:find).with(anything) { [] }
+ allow(finder).to receive(:find).with(search_query(sdk, filename)) { [instance_double(Gitlab::Search::FoundBlob)] }
+ end
+
+ it 'returns an array of unique detected targets' do
+ is_expected.to match_array result
+ end
+ end
+ end
+
+ context 'when setting string is not found' do
+ before do
+ allow(finder).to receive(:find).with(anything) { [] }
+ end
+
+ it 'returns an empty array' do
+ is_expected.to match_array []
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/record_target_platforms_service_spec.rb b/spec/services/projects/record_target_platforms_service_spec.rb
new file mode 100644
index 00000000000..85311f36428
--- /dev/null
+++ b/spec/services/projects/record_target_platforms_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::RecordTargetPlatformsService, '#execute' do
+ let_it_be(:project) { create(:project) }
+
+ subject(:execute) { described_class.new(project).execute }
+
+ context 'when project is an XCode project' do
+ before do
+ double = instance_double(Projects::AppleTargetPlatformDetectorService, execute: [:ios, :osx])
+ allow(Projects::AppleTargetPlatformDetectorService).to receive(:new) { double }
+ end
+
+ it 'creates a new setting record for the project', :aggregate_failures do
+ expect { execute }.to change { ProjectSetting.count }.from(0).to(1)
+ expect(ProjectSetting.last.target_platforms).to match_array(%w(ios osx))
+ end
+
+ it 'returns array of detected target platforms' do
+ expect(execute).to match_array %w(ios osx)
+ end
+
+ context 'when a project has an existing setting record' do
+ before do
+ create(:project_setting, project: project, target_platforms: saved_target_platforms)
+ end
+
+ def project_setting
+ ProjectSetting.find_by_project_id(project.id)
+ end
+
+ context 'when target platforms changed' do
+ let(:saved_target_platforms) { %w(tvos) }
+
+ it 'updates' do
+ expect { execute }.to change { project_setting.target_platforms }.from(%w(tvos)).to(%w(ios osx))
+ end
+
+ it { is_expected.to match_array %w(ios osx) }
+ end
+
+ context 'when target platforms are the same' do
+ let(:saved_target_platforms) { %w(osx ios) }
+
+ it 'does not update' do
+ expect { execute }.not_to change { project_setting.updated_at }
+ end
+ end
+ end
+ end
+
+ context 'when project is not an XCode project' do
+ before do
+ double = instance_double(Projects::AppleTargetPlatformDetectorService, execute: [])
+ allow(Projects::AppleTargetPlatformDetectorService).to receive(:new).with(project) { double }
+ end
+
+ it 'does nothing' do
+ expect { execute }.not_to change { ProjectSetting.count }
+ end
+
+ it { is_expected.to be_nil }
+ end
+end
diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb
new file mode 100644
index 00000000000..eb53e3f8608
--- /dev/null
+++ b/spec/workers/projects/record_target_platforms_worker_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::RecordTargetPlatformsWorker do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:swift) { create(:programming_language, name: 'Swift') }
+ let_it_be(:objective_c) { create(:programming_language, name: 'Objective-C') }
+ let_it_be(:project) { create(:project, :repository, detected_repository_languages: true) }
+
+ let(:worker) { described_class.new }
+ let(:service_result) { %w(ios osx watchos) }
+ let(:service_double) { instance_double(Projects::RecordTargetPlatformsService, execute: service_result) }
+ let(:lease_key) { "#{described_class.name.underscore}:#{project.id}" }
+ let(:lease_timeout) { described_class::LEASE_TIMEOUT }
+
+ subject(:perform) { worker.perform(project.id) }
+
+ before do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ end
+
+ shared_examples 'performs detection' do
+ it 'creates and executes a Projects::RecordTargetPlatformService instance for the project', :aggregate_failures do
+ expect(Projects::RecordTargetPlatformsService).to receive(:new).with(project) { service_double }
+ expect(service_double).to receive(:execute)
+
+ perform
+ end
+
+ it 'logs extra metadata on done', :aggregate_failures do
+ expect(Projects::RecordTargetPlatformsService).to receive(:new).with(project) { service_double }
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:target_platforms, service_result)
+
+ perform
+ end
+ end
+
+ shared_examples 'does nothing' do
+ it 'does nothing' do
+ expect(Projects::RecordTargetPlatformsService).not_to receive(:new)
+
+ perform
+ end
+ end
+
+ context 'when project uses Swift programming language' do
+ let!(:repository_language) { create(:repository_language, project: project, programming_language: swift) }
+
+ include_examples 'performs detection'
+ end
+
+ context 'when project uses Objective-C programming language' do
+ let!(:repository_language) { create(:repository_language, project: project, programming_language: objective_c) }
+
+ include_examples 'performs detection'
+ end
+
+ context 'when the project does not contain programming languages for Apple platforms' do
+ it_behaves_like 'does nothing'
+ end
+
+ context 'when project is not found' do
+ it 'does nothing' do
+ expect(Projects::RecordTargetPlatformsService).not_to receive(:new)
+
+ worker.perform(non_existing_record_id)
+ end
+ end
+
+ context 'when exclusive lease cannot be obtained' do
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
+
+ it_behaves_like 'does nothing'
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ it 'overrides #lease_release? to return false' do
+ expect(worker.send(:lease_release?)).to eq false
+ end
+end