-
- {{
- s__('BulkImport|No parent')
- }}
-
-
-
- {{ s__('BulkImport|Existing groups') }}
-
-
- {{ ns.fullPath }}
-
-
-
+ @select="onImportTargetSelect"
+ />
+
-
- {{ model.name }} / {{ model.version }}
-
-
+
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
new file mode 100644
index 00000000000..4f91f0939a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+ {{ model.name }}
+
+
+
+ {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
+
+
+
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
index 6f61b0aff46..9210d816373 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
@@ -1,4 +1,16 @@
-import { s__ } from '~/locale';
+import { s__, n__, sprintf } from '~/locale';
export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelVersionCountMessage = (version, versionCount) => {
+ if (!versionCount) return s__('MlModelRegistry|No registered versions');
+
+ const message = n__(
+ 'MlModelRegistry|%{version} · No other versions',
+ 'MlModelRegistry|%{version} · %{versionCount} versions',
+ versionCount,
+ );
+
+ return sprintf(message, { version, versionCount });
+};
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
index cf9108992be..478c5847b41 100644
--- a/app/assets/javascripts/notes/components/email_participants_warning.vue
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -1,11 +1,12 @@
+
+
+
+
{{ $options.i18n.pageTitle }}
+
+
+ {{ content }}
+
+
+
+
+
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
new file mode 100644
index 00000000000..766c7e96d14
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createOrganization($input: LocalCreateOrganizationInput!) {
+ createOrganization(input: $input) @client {
+ organization {
+ name
+ path
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
new file mode 100644
index 00000000000..f708c4ad162
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+input LocalCreateOrganizationInput {
+ name: String
+ path: String
+}
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
new file mode 100644
index 00000000000..a65603227f6
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const initOrganizationsNew = () => {
+ const el = document.getElementById('js-organizations-new');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationNewRoot',
+ apolloProvider,
+ provide: {
+ organizationsPath,
+ rootUrl,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
new file mode 100644
index 00000000000..db33f240966
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $options.i18n.createOrganization
+ }}
+ {{ $options.i18n.cancel }}
+
+
+
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 318b41647bc..9f7e9b22e1d 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,4 +1,9 @@
-import { organizations, organizationProjects, organizationGroups } from '../../mock_data';
+import {
+ organizations,
+ organizationProjects,
+ organizationGroups,
+ createOrganizationResponse,
+} from '../../mock_data';
const simulateLoading = () => {
return new Promise((resolve) => {
@@ -28,4 +33,12 @@ export default {
};
},
},
+ Mutation: {
+ createOrganization: async () => {
+ // Simulate API loading
+ await simulateLoading();
+
+ return createOrganizationResponse;
+ },
+ },
};
diff --git a/app/assets/javascripts/pages/organizations/organizations/new/index.js b/app/assets/javascripts/pages/organizations/organizations/new/index.js
new file mode 100644
index 00000000000..ab23fbf155d
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/new/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsNew } from '~/organizations/new';
+
+initOrganizationsNew();
diff --git a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
index b5afe92316a..6b70e9f3ed9 100644
--- a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
@@ -57,6 +57,7 @@ export default {
:next-text="$options.i18n.nextPageButtonLabel"
:prev-button-link="previousPageLink"
:next-button-link="nextPageLink"
+ class="gl-mt-4"
/>
diff --git a/app/assets/stylesheets/page_bundles/organizations.scss b/app/assets/stylesheets/page_bundles/organizations.scss
index 7591be064c6..1f1d127a82a 100644
--- a/app/assets/stylesheets/page_bundles/organizations.scss
+++ b/app/assets/stylesheets/page_bundles/organizations.scss
@@ -4,3 +4,7 @@
.organization-row .organization-description p {
@include gl-mb-0;
}
+
+.organization-root-path {
+ max-width: 40vw;
+}
diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb
index 8569d26945c..57900165ad1 100644
--- a/app/components/projects/ml/models_index_component.rb
+++ b/app/components/projects/ml/models_index_component.rb
@@ -25,6 +25,7 @@ module Projects
{
name: m.name,
version: m.latest_version_name,
+ version_count: m.version_count,
path: m.latest_package_path
}
end
diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index 0ae99782cd3..585b35981a6 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -13,11 +13,13 @@ module Packages
project.packages.installable
end
- def packages_visible_to_user(user, within_group:)
+ def packages_visible_to_user(user, within_group:, with_package_registry_enabled: false)
return ::Packages::Package.none unless within_group
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
projects = projects_visible_to_reporters(user, within_group: within_group)
+ projects = projects.with_package_registry_enabled if with_package_registry_enabled
+
::Packages::Package.for_projects(projects.select(:id)).installable
end
diff --git a/app/finders/packages/npm/packages_for_user_finder.rb b/app/finders/packages/npm/packages_for_user_finder.rb
index f42e49f9184..dc1d3b6e7fe 100644
--- a/app/finders/packages/npm/packages_for_user_finder.rb
+++ b/app/finders/packages/npm/packages_for_user_finder.rb
@@ -3,6 +3,8 @@
module Packages
module Npm
class PackagesForUserFinder < ::Packages::GroupOrProjectPackageFinder
+ extend ::Gitlab::Utils::Override
+
def execute
packages
end
@@ -13,6 +15,11 @@ module Packages
base.npm
.with_name(@params[:package_name])
end
+
+ override :group_packages
+ def group_packages
+ packages_visible_to_user(@current_user, within_group: @project_or_group, with_package_registry_enabled: true)
+ end
end
end
end
diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb
index 004fd20403d..1e407ba4aa4 100644
--- a/app/finders/projects/ml/model_finder.rb
+++ b/app/finders/projects/ml/model_finder.rb
@@ -11,6 +11,7 @@ module Projects
::Ml::Model
.by_project(@project)
.including_latest_version
+ .with_version_count
end
end
end
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index a88be976337..510561ec614 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -30,10 +30,6 @@ module IntegrationsHelper
_("Alert")
when "incident"
_("Incident")
- when "group_mention"
- _("Group mention in public")
- when "group_confidential_mention"
- _("Group mention in private")
end
end
# rubocop:enable Metrics/CyclomaticComplexity
@@ -295,10 +291,6 @@ module IntegrationsHelper
s_("ProjectService|Trigger event when a new, unique alert is recorded.")
when "incident", "incident_events"
s_("ProjectService|Trigger event when an incident is created.")
- when "group_mention"
- s_("ProjectService|Trigger event when a group is mentioned in a public context.")
- when "group_confidential_mention"
- s_("ProjectService|Trigger event when a group is mentioned in a confidential context.")
when "build_events"
s_("ProjectService|Trigger event when a build is created.")
when "archive_trace_events"
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index 0f760d87173..5d89bb93000 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -16,6 +16,13 @@ module Organizations
}.merge(shared_groups_and_projects_app_data).to_json
end
+ def organization_new_app_data
+ {
+ organizations_path: organizations_path,
+ root_url: root_url
+ }.to_json
+ end
+
def organization_groups_and_projects_app_data
shared_groups_and_projects_app_data.to_json
end
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
new file mode 100644
index 00000000000..2bc33a6f050
--- /dev/null
+++ b/app/models/ci/catalog/components_project.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Ci
+ module Catalog
+ class ComponentsProject
+ # ComponentsProject is a type of Catalog Resource which contains one or more
+ # CI/CD components.
+ # It is responsible for retrieving the data of a component file, including the content, name, and file path.
+
+ TEMPLATE_FILE = 'template.yml'
+ TEMPLATES_DIR = 'templates'
+ TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+
+ ComponentData = Struct.new(:content, :path, keyword_init: true)
+
+ def initialize(project, sha = project&.default_branch)
+ @project = project
+ @sha = sha
+ end
+
+ def fetch_component_paths(sha)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ end
+
+ def extract_component_name(path)
+ return unless path.match?(TEMPLATE_PATH_REGEX)
+
+ dirname = File.dirname(path)
+ filename = File.basename(path, '.*')
+
+ if dirname == TEMPLATES_DIR
+ filename
+ else
+ File.basename(dirname)
+ end
+ end
+
+ def extract_inputs(blob)
+ result = Gitlab::Ci::Config::Yaml::Loader.new(blob).load_uninterpolated_yaml
+
+ raise result.error_class, result.error unless result.valid?
+
+ result.inputs
+ end
+
+ def fetch_component(component_name)
+ path = simple_template_path(component_name)
+ content = fetch_content(path)
+
+ if content.nil?
+ path = complex_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ if content.nil?
+ path = legacy_template_path(component_name)
+ content = fetch_content(path)
+ end
+
+ ComponentData.new(content: content, path: path)
+ end
+
+ private
+
+ attr_reader :project, :sha
+
+ def fetch_content(component_path)
+ project.repository.blob_data_at(sha, component_path)
+ end
+
+ # A simple template consists of a single file
+ def simple_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, "#{component_name}.yml")
+ end
+
+ # A complex template is directory-based and may consist of multiple files.
+ # Given a path like "my-org/sub-group/the-project/templates/component"
+ # returns the entry point path: "templates/component/template.yml".
+ def complex_template_path(component_name)
+ # TODO: Extract this line and move to fetch_content once we remove legacy fetching
+ return unless component_name.index('/').nil?
+
+ File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE)
+ end
+
+ def legacy_template_path(component_name)
+ File.join(component_name, TEMPLATE_FILE).delete_prefix('/')
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index e97c7e5e738..2feae29f627 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -125,5 +125,9 @@ module Integrations
Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
end
+
+ def avatar_url
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg')
+ end
end
end
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index 0680bb0d381..27f03ed5857 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -19,6 +19,11 @@ module Ml
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :with_version_count, -> {
+ left_outer_joins(:versions)
+ .select("ml_models.*, count(ml_model_versions.id) as version_count")
+ .group(:id)
+ }
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
diff --git a/app/models/project.rb b/app/models/project.rb
index 6c12c85d45d..fd226d23e77 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -753,6 +753,7 @@ class Project < ApplicationRecord
scope :service_desk_enabled, -> { where(service_desk_enabled: true) }
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+ scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_issues_or_mrs_available_for_user, -> (user) do
diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb
index 45abb415174..4f09d47b530 100644
--- a/app/services/ci/components/fetch_service.rb
+++ b/app/services/ci/components/fetch_service.rb
@@ -5,8 +5,6 @@ module Ci
class FetchService
include Gitlab::Utils::StrongMemoize
- TEMPLATE_FILE = 'template.yml'
-
COMPONENT_PATHS = [
::Gitlab::Ci::Components::InstancePath
].freeze
@@ -23,11 +21,16 @@ module Ci
reason: :unsupported_path)
end
- component_path = component_path_class.new(address: address, content_filename: TEMPLATE_FILE)
- content = component_path.fetch_content!(current_user: current_user)
+ component_path = component_path_class.new(address: address)
+ result = component_path.fetch_content!(current_user: current_user)
- if content.present?
- ServiceResponse.success(payload: { content: content, path: component_path })
+ if result
+ ServiceResponse.success(payload: {
+ content: result.content,
+ path: result.path,
+ project: component_path.project,
+ sha: component_path.sha
+ })
else
ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found)
end
diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb
index 1316b2546b9..f4d3f3e72d3 100644
--- a/app/services/ml/find_or_create_model_version_service.rb
+++ b/app/services/ml/find_or_create_model_version_service.rb
@@ -11,7 +11,6 @@ module Ml
def execute
model = Ml::FindOrCreateModelService.new(project, name).execute
-
Ml::ModelVersion.find_or_create!(model, version, package)
end
diff --git a/app/views/organizations/organizations/new.html.haml b/app/views/organizations/organizations/new.html.haml
index 4d7f552c87b..1a6c5a79ff6 100644
--- a/app/views/organizations/organizations/new.html.haml
+++ b/app/views/organizations/organizations/new.html.haml
@@ -1,3 +1,6 @@
+- add_page_specific_style 'page_bundles/organizations'
- page_title s_('Organization|New organization')
- header_title _("Your work"), root_path
- add_to_breadcrumbs s_('Organization|Organizations'), organizations_path
+
+#js-organizations-new{ data: { app_data: organization_new_app_data } }
diff --git a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
index 98bb259db0a..8bcbe9d6c9f 100644
--- a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
+++ b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Ci
module MergeRequests
class AddTodoWhenBuildFailsWorker
diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb
index 61e3c1544bd..cdf429a8be5 100644
--- a/app/workers/concerns/auto_devops_queue.rb
+++ b/app/workers/concerns/auto_devops_queue.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-#
+
module AutoDevopsQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb
index 23e58b5182b..9a3d518dda8 100644
--- a/app/workers/concerns/chaos_queue.rb
+++ b/app/workers/concerns/chaos_queue.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-#
+
module ChaosQueue
extend ActiveSupport::Concern
diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb
index 4b5ce8a01f6..b4d884f914d 100644
--- a/app/workers/concerns/limited_capacity/job_tracker.rb
+++ b/app/workers/concerns/limited_capacity/job_tracker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module LimitedCapacity
class JobTracker # rubocop:disable Scalability/IdempotentWorker
include Gitlab::Utils::StrongMemoize
diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb
index 58b0f5496f4..417af4c7172 100644
--- a/app/workers/database/batched_background_migration/ci_database_worker.rb
+++ b/app/workers/database/batched_background_migration/ci_database_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Database
module BatchedBackgroundMigration
class CiDatabaseWorker # rubocop:disable Scalability/IdempotentWorker
diff --git a/app/workers/gitlab/github_gists_import/import_gist_worker.rb b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
index 60e4c8fdad6..151788150dd 100644
--- a/app/workers/gitlab/github_gists_import/import_gist_worker.rb
+++ b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
@@ -106,9 +106,9 @@ module Gitlab
def error(user_id, error_message, github_identifiers)
attributes = {
user_id: user_id,
- github_identifiers: github_identifiers,
+ external_identifiers: github_identifiers,
message: 'importer failed',
- 'error.message': error_message
+ 'exception.message': error_message
}
Gitlab::GithubImport::Logger.error(structured_payload(attributes))
@@ -120,7 +120,7 @@ module Gitlab
attributes = {
user_id: user_id,
message: message,
- github_identifiers: gist_id
+ external_identifiers: gist_id
}
Gitlab::GithubImport::Logger.info(structured_payload(attributes))
diff --git a/app/workers/gitlab/github_gists_import/start_import_worker.rb b/app/workers/gitlab/github_gists_import/start_import_worker.rb
index 33c91611719..f7d3eb1d759 100644
--- a/app/workers/gitlab/github_gists_import/start_import_worker.rb
+++ b/app/workers/gitlab/github_gists_import/start_import_worker.rb
@@ -51,7 +51,7 @@ module Gitlab
end
def log_error_and_raise!(user_id, error)
- logger.error(structured_payload(user_id: user_id, message: 'import failed', 'error.message': error.message))
+ logger.error(structured_payload(user_id: user_id, message: 'import failed', 'exception.message': error.message))
raise(error)
end
diff --git a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
index 01979b2029f..93d670e1b8b 100644
--- a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
+++ b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Gitlab
module Import
class StuckProjectImportJobsWorker # rubocop:disable Scalability/IdempotentWorker
diff --git a/app/workers/projects/after_import_worker.rb b/app/workers/projects/after_import_worker.rb
index 06211b2d991..47bd07d0850 100644
--- a/app/workers/projects/after_import_worker.rb
+++ b/app/workers/projects/after_import_worker.rb
@@ -31,7 +31,7 @@ module Projects
message: 'Project housekeeping failed',
project_full_path: @project.full_path,
project_id: @project.id,
- 'error.message' => e.message
+ 'exception.message' => e.message
)
end
diff --git a/config/feature_flags/development/print_wiki.yml b/config/feature_flags/development/print_wiki.yml
index e04d7dd84bf..75305425deb 100644
--- a/config/feature_flags/development/print_wiki.yml
+++ b/config/feature_flags/development/print_wiki.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414691
milestone: '16.3'
type: development
group: group::knowledge
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/ops/global_search_epics_tab.yml b/config/feature_flags/ops/global_search_epics_tab.yml
new file mode 100644
index 00000000000..42067e9ad93
--- /dev/null
+++ b/config/feature_flags/ops/global_search_epics_tab.yml
@@ -0,0 +1,8 @@
+---
+name: global_search_epics_tab
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130146
+rollout_issue_url:
+milestone: '16.5'
+type: ops
+group: group::global search
+default_enabled: false
diff --git a/doc/administration/audit_event_streaming/graphql_api.md b/doc/administration/audit_event_streaming/graphql_api.md
index 905276a8257..6e1a3424929 100644
--- a/doc/administration/audit_event_streaming/graphql_api.md
+++ b/doc/administration/audit_event_streaming/graphql_api.md
@@ -338,13 +338,14 @@ To enable streaming and add a configuration, use the
```graphql
mutation {
- googleCloudLoggingConfigurationCreate(input: { groupPath: "my-group", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events" } ) {
+ googleCloudLoggingConfigurationCreate(input: { groupPath: "my-group", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "destination-name" } ) {
errors
googleCloudLoggingConfiguration {
id
googleProjectIdName
logIdName
clientEmail
+ name
}
errors
}
@@ -377,6 +378,7 @@ query {
logIdName
googleProjectIdName
clientEmail
+ name
}
}
}
@@ -397,12 +399,12 @@ Prerequisite:
To update streaming configuration for a top-level group, use the
`googleCloudLoggingConfigurationUpdate` mutation type. You can retrieve the configuration ID
-by [listing all the external destinations](#list-streaming-destinations).
+by [listing all the external destinations](#list-google-cloud-logging-configurations).
```graphql
mutation {
googleCloudLoggingConfigurationUpdate(
- input: {id: "gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events"}
+ input: {id: "gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "updated-destination-name" }
) {
errors
googleCloudLoggingConfiguration {
@@ -410,6 +412,7 @@ mutation {
logIdName
googleProjectIdName
clientEmail
+ name
}
}
}
@@ -432,7 +435,7 @@ Prerequisite:
Users with the Owner role for a group can delete streaming configurations using the
`googleCloudLoggingConfigurationDestroy` mutation type. You can retrieve the configurations ID
-by [listing all the streaming destinations](#list-streaming-destinations) for the group.
+by [listing all the streaming destinations](#list-google-cloud-logging-configurations) for the group.
```graphql
mutation {
@@ -453,9 +456,13 @@ Streaming configuration is deleted if:
> - [Feature flag `ff_external_audit_events`](https://gitlab.com/gitlab-org/gitlab/-/issues/393772) enabled by default in GitLab 16.2.
> - Instance streaming destinations [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/393772) in GitLab 16.4. [Feature flag `ff_external_audit_events`](https://gitlab.com/gitlab-org/gitlab/-/issues/417708) removed.
+Manage streaming destinations for an entire instance.
+
+### HTTP destinations
+
Manage HTTP streaming destinations for an entire instance.
-### Add a new HTTP destination
+#### Add a new HTTP destination
Add a new HTTP streaming destination to an instance.
@@ -528,7 +535,7 @@ mutation {
The header is created if the returned `errors` object is empty.
-### List streaming destinations
+#### List streaming destinations
List all HTTP streaming destinations for an instance.
@@ -565,7 +572,7 @@ If the resulting list is empty, then audit streaming is not enabled for the inst
You need the ID values returned by this query for the update and delete mutations.
-### Update streaming destinations
+#### Update streaming destinations
Update a HTTP streaming destination for an instance.
@@ -619,7 +626,7 @@ mutation {
The header is updated if the returned `errors` object is empty.
-### Delete streaming destinations
+#### Delete streaming destinations
Delete streaming destinations for an entire instance.
@@ -660,7 +667,7 @@ mutation {
The header is deleted if the returned `errors` object is empty.
-### Event type filters
+#### Event type filters
> Event type filters API [introduced](https://gitlab.com/groups/gitlab-org/-/epics/10868) in GitLab 16.2.
@@ -669,7 +676,7 @@ If the feature is enabled with no filters, the destination receives all audit ev
A streaming destination that has an event type filter set has a **filtered** (**{filter}**) label.
-#### Use the API to add an event type filter
+##### Use the API to add an event type filter
Prerequisites:
@@ -693,7 +700,7 @@ Event type filters are added if:
- The returned `errors` object is empty.
- The API responds with `200 OK`.
-#### Use the API to remove an event type filter
+##### Use the API to remove an event type filter
Prerequisites:
@@ -716,3 +723,134 @@ Event type filters are removed if:
- The returned `errors` object is empty.
- The API responds with `200 OK`.
+
+### Google Cloud Logging destinations
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11303) in GitLab 16.5.
+
+Manage Google Cloud Logging destinations for an entire instance.
+
+Before setting up Google Cloud Logging streaming audit events, you must satisfy [the prerequisites](index.md#prerequisites).
+
+#### Add a new Google Cloud Logging destination
+
+Add a new Google Cloud Logging configuration destination to an instance.
+
+Prerequisites:
+
+- You have administrator access to the instance.
+- You have a Google Cloud project with the necessary permissions to create service accounts and enable Google Cloud Logging.
+
+To enable streaming and add a configuration, use the
+`instanceGoogleCloudLoggingConfigurationCreate` mutation in the GraphQL API.
+
+```graphql
+mutation {
+ instanceGoogleCloudLoggingConfigurationCreate(input: { googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "destination-name" } ) {
+ errors
+ googleCloudLoggingConfiguration {
+ id
+ googleProjectIdName
+ logIdName
+ clientEmail
+ name
+ }
+ errors
+ }
+}
+```
+
+Event streaming is enabled if:
+
+- The returned `errors` object is empty.
+- The API responds with `200 OK`.
+
+#### List Google Cloud Logging configurations
+
+List all Google Cloud Logging configuration destinations for an instance.
+
+Prerequisite:
+
+- You have administrator access to the instance.
+
+You can view a list of streaming configurations for an instance using the `instanceGoogleCloudLoggingConfigurations` query
+type.
+
+```graphql
+query {
+ instanceGoogleCloudLoggingConfigurations {
+ nodes {
+ id
+ logIdName
+ googleProjectIdName
+ clientEmail
+ name
+ }
+ }
+}
+```
+
+If the resulting list is empty, audit streaming is not enabled for the instance.
+
+You need the ID values returned by this query for the update and delete mutations.
+
+#### Update Google Cloud Logging configurations
+
+Update the Google Cloud Logging configuration destinations for an instance.
+
+Prerequisite:
+
+- You have administrator access to the instance.
+
+To update streaming configuration for an instance, use the
+`instanceGoogleCloudLoggingConfigurationUpdate` mutation type. You can retrieve the configuration ID
+by [listing all the external destinations](#list-google-cloud-logging-configurations-1).
+
+```graphql
+mutation {
+ instanceGoogleCloudLoggingConfigurationUpdate(
+ input: {id: "gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "updated-google-id", clientEmail: "updated@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "updated name"}
+ ) {
+ errors
+ instanceGoogleCloudLoggingConfiguration {
+ id
+ logIdName
+ googleProjectIdName
+ clientEmail
+ name
+ }
+ }
+}
+```
+
+Streaming configuration is updated if:
+
+- The returned `errors` object is empty.
+- The API responds with `200 OK`.
+
+#### Delete Google Cloud Logging configurations
+
+Delete streaming destinations for an instance.
+
+When the last destination is successfully deleted, streaming is disabled for the instance.
+
+Prerequisite:
+
+- You have administrator access to the instance.
+
+To delete streaming configurations, use the
+`instanceGoogleCloudLoggingConfigurationDestroy` mutation type. You can retrieve the configurations ID
+by [listing all the streaming destinations](#list-google-cloud-logging-configurations-1) for the instance.
+
+```graphql
+mutation {
+ instanceGoogleCloudLoggingConfigurationDestroy(input: { id: "gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/1" }) {
+ errors
+ }
+}
+```
+
+Streaming configuration is deleted if:
+
+- The returned `errors` object is empty.
+- The API responds with `200 OK`.
diff --git a/doc/administration/settings/user_and_ip_rate_limits.md b/doc/administration/settings/user_and_ip_rate_limits.md
index 09ddb784191..22b16d01394 100644
--- a/doc/administration/settings/user_and_ip_rate_limits.md
+++ b/doc/administration/settings/user_and_ip_rate_limits.md
@@ -103,6 +103,20 @@ To use a custom response:
1. In the **Plain-text response to send to clients that hit a rate limit** text box,
add the plain-text response message.
+## Maximum authenticated requests to `project/:id/jobs` per minute
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129319) in GitLab 16.5.
+
+To reduce timeouts, the `project/:id/jobs` endpoint has a default [rate limit](../../security/rate_limits.md#project-jobs-api-endpoint) of 600 calls per authenticated user.
+
+To modify the maximum number of requests:
+
+1. On the left sidebar, select **Search or go to**.
+1. Select **Admin Area**.
+1. Select **Settings > Network**.
+1. Expand **User and IP rate limits**.
+1. Update the **Maximum authenticated requests to `project/:id/jobs` per minute** value.
+
## Response headers
> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/731) in GitLab 13.8, the `RateLimit` headers. `Retry-After` was introduced in an earlier version.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 95d6247fea4..03877c6c489 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -121,7 +121,8 @@ Example response:
"jira_connect_proxy_url": null,
"silent_mode_enabled": false,
"package_registry_allow_anyone_to_pull_option": true,
- "bulk_import_max_download_file_size": 5120
+ "bulk_import_max_download_file_size": 5120,
+ "project_jobs_api_rate_limit": 600
}
```
@@ -265,7 +266,8 @@ Example response:
"silent_mode_enabled": false,
"security_policy_global_group_approvers_enabled": true,
"package_registry_allow_anyone_to_pull_option": true,
- "bulk_import_max_download_file_size": 5120
+ "bulk_import_max_download_file_size": 5120,
+ "project_jobs_api_rate_limit": 600
}
```
@@ -510,6 +512,7 @@ listed in the descriptions of the relevant settings.
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `project_export_enabled` | boolean | no | Enable project export. |
+| `project_jobs_api_rate_limit` | integer |no | Maximum authenticated requests to `/project/:id/jobs` per minute. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129319) in GitLab 16.5. Default: 600.
| `projects_api_rate_limit_unauthenticated` | integer | no | [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10. Max number of requests per 10 minutes per IP address for unauthenticated requests to the [list all projects API](projects.md#list-all-projects). Default: 400. To disable throttling set to 0.|
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. |
diff --git a/doc/architecture/blueprints/bundle_uri/index.md b/doc/architecture/blueprints/bundle_uri/index.md
new file mode 100644
index 00000000000..a056649a798
--- /dev/null
+++ b/doc/architecture/blueprints/bundle_uri/index.md
@@ -0,0 +1,216 @@
+---
+status: proposed
+creation-date: "2023-08-04"
+authors: [ "@toon" ]
+coach: ""
+approvers: [ "@mjwood", "@jcaigitlab" ]
+owning-stage: "~devops::systems"
+participating-stages: []
+---
+
+
+
+
+# Utilize bundle-uri to reduce Gitaly CPU load
+
+## Summary
+
+[bundle-URI](https://git-scm.com/docs/bundle-uri) is a fairly new concept
+in Git that allows the client to download one or more bundles in order to
+bootstrap the object database in advance of fetching the remaining objects from
+a remote. By having the client download static files from a simple HTTP(S)
+server in advance, the work that needs to be done on the remote side is reduced.
+
+Git bundles are files that store a packfile along with some extra metadata,
+including a set of refs and a (possibly empty) set of necessary commits. When a
+user clones a repository, the server can advertise one or more URIs that serve
+these bundles. The client can download these to populate the Git object
+database. After it has done this, the negotiation process between server and
+client start to see which objects need be fetched. When the client pre-populated
+the database with some data from the bundles, the negotiation and transfer of
+objects from the server is reduced, putting less load on the server's CPU.
+
+## Motivation
+
+When a user pushes changes, it usually kicks off a CI pipeline with
+a bunch of jobs. When the CI runners all clone the repository from scratch,
+if they use [`git clone`](/ee/ci/pipelines/settings.md#choose-the-default-git-strategy),
+they all start negotiating with the server what they need to clone. This is
+really CPU intensive for the server.
+
+Some time ago we've introduced the
+[pack-objects](/ee/administration/gitaly/configure_gitaly.md#pack-objects-cache),
+but it has some pitfalls. When the tip of a branch changes, a new packfile needs
+to be calculated, and the cache needs to be refreshed.
+
+Git bundles are more flexible. It's not a big issue if the bundle doesn't have
+all the most recent objects. When it contains a fairly recent state, but is
+missing the latest refs, the client (that is, the CI runner) will do a "catch up" and
+fetch additional objects after applying the bundle. The set of objects it has to
+fetch from will Gitaly be a lot smaller.
+
+### Goals
+
+Reduce the work that needs to be done on the Gitaly servers when a client clones
+a repository. This is particularly useful for CI build farms, which generate a
+lot of traffic on each commit that's pushed to the server.
+
+With the use bundles, the server has to craft a smaller delta packfiles
+compared to the pack files that contain all the objects when no bundles are
+used. This reduces the load on the CPU of the server. This has a benefit on the
+packfile cache as well, because now the packfiles are smaller and faster to
+generate, reducing the chances on cache misses.
+
+### Non-Goals
+
+Using bundle-URIs will **not** reduce the size of repositories stored on disk.
+This feature will not be used to offload repositories, neither fully nor
+partially, from the Gitaly node to some cloud storage. In contrary, because
+bundles are stored elsewhere, some data is duplicated, and will cause increased
+storage costs.
+
+In this phase it's not the goal to boost performance for incremental
+fetches. When the client has already cloned the repository, bundles won't be
+used to optimize fetches new data.
+
+Currently bundle-URI is not fully compatible with shallow clones, therefore
+we'll leave that out of scope. More info about that in
+[Git issue #170](https://gitlab.com/gitlab-org/git/-/issues/170).
+
+## Proposal
+
+When a client clones a repository, Gitaly advertises a bundle URI. This URI
+points to a bundle that's refreshed on a regular interval, for example during
+housekeeping. For each repository only one bundle will exist, so when a new one
+is created, the old one is invalidated.
+
+The bundles will be stored on a cloud Object Storage. To use bundles, the
+administrator should configure this in Gitaly.
+
+## Design and implementation details
+
+When a client initiates a `git clone`, on the server-side Gitaly spawns a
+`git upload-pack` process. Gitaly can pass along additional Git
+configuration. To make `git upload-pack` advertise bundle URIs, it should pass
+the following configuration:
+
+- `uploadpack.advertiseBundleURIs` :: This should be set to `true` to enable to
+ use of advertised bundles.
+- `bundle.version` :: At the moment only `1` is accepted.
+- `bundle.mode` :: This can be either `any` or `all`. Since we only want to use
+ bundles for the initial clone, `any` is advised.
+- `bundle.
.uri` :: This is the actual URI of the bundle identified with
+ ``. Initially we will only have one bundle per repository.
+
+### Enable the use of advertised bundles on the client-side
+
+The current version of Git does not use the advertised bundles by default when
+cloning or fetching from a remote.
+Luckily, we control most of the CI runners ourself. So to use bundle URI, we can
+modify the Git configuration used by the runners and set
+`transfer.bundleURI=true`.
+
+### Access control
+
+We don't want to leak data from private repositories through public HTTP(S)
+hosts. There are a few options for how we can overcome this:
+
+- Only activate the use of bundle-URI on public repositories.
+- Use a solution like [signed-URLs](https://cloud.google.com/cdn/docs/using-signed-urls).
+
+#### Public repositories only
+
+Gitaly itself does not know if a project, and its repository, is public, so to
+determine whether bundles can be used, GitLab Rails has to tell Gitaly. It's
+complex to pass this information to Gitaly, and using this approach will make
+the feature only available for public projects, so we will not proceed with this
+solution.
+
+#### Signed URLs
+
+The use of [signed-URLs](https://cloud.google.com/cdn/docs/using-signed-urls) is
+another option to control access to the bundles. This feature, provided by
+Google Cloud, allows Gitaly to create a URI that has a short lifetime.
+
+The downside to this approach is it depends on a feature that is
+cloud-specific, so each cloud provider might provide such feature slightly
+different, or not have it. But we want to roll this feature out on GitLab.com
+first, which is hosted on Google Cloud, so for a first iteration we will use
+this.
+
+### Bundle creation
+
+#### Use server-side backups
+
+At the moment Gitaly knows how to back up repositories into bundles onto cloud
+storage. The [documentation](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/gitaly-backup.md#user-content-server-side-backups)
+describes how to use it.
+
+For the initial implementation of bundle-URI we can piggy-back onto this
+feature. An admin should create backups for the repositories they want to use
+bundle-URI. With the existing configuration for backups, Gitaly can access cloud
+storage.
+
+#### As part of housekeeping
+
+Gitaly has a housekeeping worker that daily looks for repositories to optimize.
+Ideally we create a bundle right after the housekeeping (that is, garbage collection
+and repacking) is done. This ensures the most optimal bundle file.
+
+There are a few things to keep in mind when automatically creating bundles:
+
+- **Does the bundle need to be recreated?** When there wasn't much activity on
+ the repository it's probably not needed to create a new bundle file, as the
+ client can fetch missing object directly from Gitaly anyway. The housekeeping
+ tasks uses various heuristics to determine which strategy is taken for the
+ housekeeping job, we can reuse parts of this logic in the creation of bundles.
+- **Is it even needed to create a bundle?** Some repositories might be very
+ small, or see very little activity. Creating a bundle for these, and
+ duplicating it's data to object storage doesn't provide much value and only
+ generates cost and maintenance.
+
+#### Controlled by GitLab Rails
+
+Because bundles increase the cost on storage, we eventually want to give the
+GitLab administrator full control over the creation of bundles. To achieve this,
+bundle-URI settings will be available on the GitLab admin interface. Here the
+admin can configure per project which have bundle-URI enabled.
+
+### Configuration
+
+To use this feature, Gitaly needs to be configured. For this we'll add the
+following settings to Gitaly's configuration file:
+
+- `bundle_uri.strategy` :: This indicates which strategy should be used to
+ create and serve bundle-URIs. At the moment the only supported value is
+ "backups". When this setting to that value, Gitaly checks if a server-side
+ backup is available and use that.
+- `bundle_uri.sign_urls` :: When set to true, the cloud storage URLs are not
+ passed to the client as-is, but are transformed into a signed URL. This
+ setting is optional and only support Google Cloud Storage (for now).
+
+The credentials to access cloud storage are reused as described in the Gitaly
+Backups documentation.
+
+### Storing metadata
+
+For now all metadata needed to store bundles on the cloud is managed by Gitaly
+server-side backups.
+
+### Bundle cleanup
+
+At some point the admin might decide to cleanup bundles for one or more
+repositories, an admin command should be added for this. Because we're now only
+using bundles created by `gitaly-backup`, we leave this out of scope.
+
+### Gitaly Cluster compatibility
+
+Creating server-side backups doesn't happen through Praefect at the moment. It's
+up to the admin to address the nodes where they want to create backups from. If
+they make sure the node is up-to-date, all nodes will have access to up-to-date
+bundles and can pass proper bundle-URI parameters to the client. So no extra
+work is needed to reuse server-side backup bundles with bundle-URI.
+
+## Alternative Solutions
+
+No alternative solutions are suggested at the moment.
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 7dc633a3092..860bc8790e2 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -674,6 +674,10 @@ Do not make **GitLab** possessive (GitLab's). This guidance follows [GitLab Trad
## GitLab Dedicated
+Use **GitLab Dedicated** to refer to the product offering. It refers to a GitLab instance that's hosted and managed by GitLab for customers.
+
+GitLab Dedicated can be referred to as a single-tenant SaaS service.
+
Do not use **Dedicated** by itself. Always use **GitLab Dedicated**.
## GitLab Duo
@@ -740,16 +744,16 @@ See also:
## GitLab SaaS
-**GitLab SaaS** refers to the product license that provides access to GitLab.com. It does not refer to the
-GitLab instance managed by GitLab itself.
+Use **GitLab SaaS** to refer to the product offering.
+It does not refer to the GitLab instance, which is [GitLab.com](#gitlabcom).
## GitLab self-managed
-Use **GitLab self-managed** to refer to the product license for GitLab instances managed by customers themselves.
+Use **GitLab self-managed** to refer to the product offering. It refers to a GitLab instance managed by customers themselves.
## GitLab.com
-**GitLab.com** refers to the GitLab instance managed by GitLab itself.
+Use **GitLab.com** to refer to the URL. GitLab.com is the instance that's managed by GitLab.
## guide
@@ -1132,6 +1136,16 @@ Instead of:
- Note that you can change the settings.
+## offerings
+
+The current product offerings are:
+
+- [GitLab SaaS](#gitlab-saas)
+- [GitLab self-managed](#gitlab-self-managed)
+- [GitLab Dedicated](#gitlab-dedicated)
+
+The [tier badges](index.md#available-product-tier-badges) reflect these offerings.
+
## older
Do not use **older** when talking about version numbers.
diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md
index 024ed7b4f2c..2f916ec34b5 100644
--- a/doc/security/rate_limits.md
+++ b/doc/security/rate_limits.md
@@ -126,7 +126,7 @@ The **rate limit** is 20 calls per minute per IP address.
There is a rate limit for the endpoint `project/:id/jobs`, which is enforced to reduce timeouts when retrieving jobs.
-The **rate limit** is 600 calls per minute per authenticated user.
+The **rate limit** defaults to 600 calls per authenticated user. You can [configure the rate limit](../administration/settings/user_and_ip_rate_limits.md).
### AI action
diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md
index 0660d400b44..151792089ce 100644
--- a/doc/user/project/repository/code_suggestions/index.md
+++ b/doc/user/project/repository/code_suggestions/index.md
@@ -9,6 +9,7 @@ type: index, reference
> - [Introduced support for Google Vertex AI Codey APIs](https://gitlab.com/groups/gitlab-org/-/epics/10562) in GitLab 16.1.
> - [Removed support for GitLab native model](https://gitlab.com/groups/gitlab-org/-/epics/10752) in GitLab 16.2.
+> - [Introduced support for Code Generation](https://gitlab.com/gitlab-org/gitlab/-/issues/415583) in GitLab 16.3.
WARNING:
This feature is in [Beta](../../../../policy/experiment-beta-support.md#beta).
@@ -22,12 +23,46 @@ GitLab Duo Code Suggestions are available:
- In VS Code, Microsoft Visual Studio, JetBrains IDEs, and Neovim. You must have the corresponding GitLab extension installed.
- In the GitLab WebIDE.
-Usage of Code Suggestions is governed by the [GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/).
+
+
+
+
+
+During Beta, usage of Code Suggestions is governed by the [GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/).
Learn about [data usage when using Code Suggestions](#code-suggestions-data-usage).
+## Use Code Suggestions
+
+Prerequisites:
+
+- Code Suggestions must be enabled for [SaaS](saas.md#enable-code-suggestions) or for [self-managed](self_managed.md#enable-code-suggestions-on-self-managed-gitlab).
+- You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions).
+
+To use Code Suggestions:
+
+1. Author your code. As you type, suggestions are displayed. Code Suggestions, depending on the cursor position, either provides code snippets or completes the current line.
+1. Describe the requirements in natural language. Be concise and specific. Code Suggestions generates functions and code snippets as appropriate.
+1. To accept a suggestion, press Tab .
+1. To ignore a suggestion, keep typing as you usually would.
+1. To explicitly reject a suggestion, press esc .
+
+Things to remember:
+
+- AI is non-deterministic, so you may not get the same suggestion every time with the same input.
+- Just like product requirements, writing clear, descriptive, and specific tasks results in quality generated code.
+
+### Progressive enhancement
+
+This feature is designed as a progressive enhancement to developer's IDEs.
+Code Suggestions offer a completion if a suitable recommendation is provided to the user in a timely matter.
+In the event of a connection issue or model inference failure, the feature gracefully degrades.
+Code Suggestions do not prevent you from writing code in your IDE.
+
## Supported languages
-The best results from Code Suggestions are expected [for languages the Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_coding_languages) directly support:
+The best results from Code Suggestions are expected for languages that [Anthropic Claude](https://www.anthropic.com/product) and the [Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_coding_languages) directly support:
- C++
- C#
@@ -44,16 +79,6 @@ The best results from Code Suggestions are expected [for languages the Google Ve
- Swift
- TypeScript
-### Supported code infrastructure interfaces
-
-Code Suggestions includes [Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_code_infrastructure_interfaces) support for the following infrastructure as code interfaces:
-
-- Google Cloud CLI
-- Kubernetes Resource Model (KRM)
-- Terraform
-
-Suggestion quality for other languages and using natural language code comments to request completions may not yet result in high-quality suggestions.
-
### Supported languages in IDEs
Editor support for languages is documented in the following table.
@@ -101,13 +126,12 @@ This improvement should result in:
## Code Suggestions data usage
-Code Suggestions is a generative artificial intelligence (AI) model.
+Code Suggestions is powered by a generative AI model.
-Your personal access token enables a secure API connection to GitLab.com.
-This API connection securely transmits a context window from your IDE/editor to the Code Suggestions GitLab hosted service which calls Google Vertex AI Codey APIs,
-and the generated suggestion is transmitted back to your IDE/editor.
+Your personal access token enables a secure API connection to GitLab.com or to your GitLab instance.
+This API connection securely transmits a context window from your IDE/editor to the [GitLab AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist), a GitLab hosted service. The gateway calls the large language model APIs, and then the generated suggestion is transmitted back to your IDE/editor.
-GitLab currently leverages [Google Cloud's Vertex AI Codey API models](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview). Learn more about Google Vertex AI Codey APIs [Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance).
+GitLab selects the best-in-class large-language models for specific tasks. We use [Google Vertex AI Code Models](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview) and [Anthropic Claude](https://www.anthropic.com/product) for Code Suggestions.
### Telemetry
@@ -124,48 +148,16 @@ For self-managed instances that have enabled Code Suggestions and SaaS accounts,
### Inference window context
-Code Suggestions currently inferences against the currently opened file and has a context window of 2,048 tokens and 8,192 character limits. This limit includes content before and after the cursor, the file name, and the extension type.
-Learn more about Google Vertex AI [code-gecko](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models).
-
-The maximum number of tokens that is generated in the response is default 64. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words.
-Learn more about Google Vertex AI [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion).
+Code Suggestions inferences against the currently opened file, the content before and after the cursor, the filename, and the extension type. For more information on possible future context expansion to improve the quality of suggestions, see [epic 11669](https://gitlab.com/groups/gitlab-org/-/epics/11669).
### Training data
-Code Suggestions are routed through Google Vertex AI Codey APIs. Learn more about Google Vertex AI Codey APIs [Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance) and [Responsible AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai).
+GitLab does not train generative AI models based on private (non-public) data. The vendors we work with also do not train models based on private data.
-Google Vertex AI Codey APIs are not trained on private non-public GitLab customer or user data.
+For more information on GitLab Code Suggestions data [sub-processors](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors), see:
-Google has [shared the following](https://ai.google/discover/foundation-models/) about the data Codey models are trained on:
-
-> Codey is our family of foundational coding models built on PaLM 2. Codey was fine-tuned on a large dataset of high quality, permissively licensed code from external sources
-
-## Progressive enhancement
-
-This feature is designed as a progressive enhancement to developer's IDEs.
-Code Suggestions offer a completion if the machine learning engine can generate a recommendation.
-In the event of a connection issue or model inference failure, the feature gracefully degrades.
-Code Suggestions do not prevent you from writing code in your IDE.
-
-### Internet connectivity
-
-Code Suggestions does not work with offline environments.
-
-To use Code Suggestions:
-
-- On GitLab.com, you must have an internet connection and be able to access GitLab.
-- In GitLab 16.1 and later, on self-managed GitLab, you must have an internet connection.
-
-### Model accuracy and quality
-
-Code Suggestions can generate low-quality, incomplete, and possibly insecure code.
-We strongly encourage all beta users to leverage GitLab native
-[Code Quality Scanning](../../../../ci/testing/code_quality.md) and
-[Security Scanning](../../../application_security/index.md) capabilities.
-
-GitLab currently does not retrain Google Vertex AI Codey APIs. GitLab makes no claims
-to the accuracy or quality of Code Suggestions generated by Google Vertex AI Codey API.
-Read more about [Google Vertex AI foundation model capabilities](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models).
+- Google Vertex AI Codey APIs [data governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance) and [responsible AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai).
+- Anthropic Claude's [constitution](https://www.anthropic.com/index/claudes-constitution).
## Known limitations
@@ -178,12 +170,6 @@ However, Code Suggestions may generate suggestions that are:
- Insecure code
- Offensive or insensitive
-We are also aware of specific situations that can produce unexpected or incoherent results including:
-
-- Suggestions written in the middle of existing functions, or "fill in the middle."
-- Suggestions based on natural language code comments.
-- Suggestions that mixed programming languages in unexpected ways.
-
## Feedback
Report issues in the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/405152).
diff --git a/doc/user/project/repository/code_suggestions/saas.md b/doc/user/project/repository/code_suggestions/saas.md
index 49221ec722d..ac64aba4335 100644
--- a/doc/user/project/repository/code_suggestions/saas.md
+++ b/doc/user/project/repository/code_suggestions/saas.md
@@ -38,19 +38,4 @@ Prerequisites:
- Ensure Code Suggestions is enabled for your user and group.
- You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions).
-To use Code Suggestions:
-
-1. Author your code. As you type, suggestions are displayed. Depending on the cursor position, the extension either:
-
- - Provides entire code snippets, like generating functions.
- - Completes the current line.
-
-1. To accept a suggestion, press Tab .
-
-Suggestions are best when writing new code. Editing existing functions or 'fill in the middle' of a function may not perform as expected.
-
-GitLab is making improvements to the Code Suggestions to improve the quality. AI is non-deterministic, so you may not get the same suggestion every time with the same input.
-
-This feature is currently in [Beta](../../../../policy/experiment-beta-support.md#beta).
-Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We have built this feature to gracefully degrade and have controls in place to allow us to
-mitigate abuse or misuse. GitLab may disable this feature for any or all customers at any time at our discretion.
+[Use Code Suggestions](index.md#use-code-suggestions).
diff --git a/doc/user/project/repository/code_suggestions/self_managed.md b/doc/user/project/repository/code_suggestions/self_managed.md
index 3c149604086..ee501212027 100644
--- a/doc/user/project/repository/code_suggestions/self_managed.md
+++ b/doc/user/project/repository/code_suggestions/self_managed.md
@@ -156,22 +156,7 @@ Prerequisites:
- Code Suggestions must be enabled [for the instance](#enable-code-suggestions-on-self-managed-gitlab).
- You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions).
-To use Code Suggestions:
-
-1. Author your code. As you type, suggestions are displayed. Depending on the cursor position, the extension either:
-
- - Provides entire code snippets, like generating functions.
- - Completes the current line.
-
-1. To accept a suggestion, press Tab .
-
-Suggestions are best when writing new code. Editing existing functions or 'fill in the middle' of a function may not perform as expected.
-
-GitLab is making improvements to the Code Suggestions to improve the quality. AI is non-deterministic, so you may not get the same suggestion every time with the same input.
-
-This feature is currently in [Beta](../../../../policy/experiment-beta-support.md#beta).
-Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We have built this feature to gracefully degrade and have controls in place to allow us to
-mitigate abuse or misuse. GitLab may disable this feature for any or all customers at any time at our discretion.
+[Use Code Suggestions](index.md#use-code-suggestions).
### Data privacy
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 8c7db5ca29e..e8dfbfa675a 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -24,6 +24,7 @@ by disabling one or more [`ops` feature flags](../../development/feature_flags/i
|----------------|------------------------------------|-------------------------------------------------------------------------------------------|
| Code | `global_search_code_tab` | When enabled, global search includes code. |
| Commits | `global_search_commits_tab` | When enabled, global search includes commits. |
+| Epics | `global_search_epics_tab` | When enabled, global search includes epics. |
| Issues | `global_search_issues_tab` | When enabled, global search includes issues. |
| Merge requests | `global_search_merge_requests_tab` | When enabled, global search includes merge requests. |
| Users | `global_search_users_tab` | When enabled, global search includes users. |
diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb
index 17c784c4d54..551284d9099 100644
--- a/lib/gitlab/ci/components/instance_path.rb
+++ b/lib/gitlab/ci/components/instance_path.rb
@@ -7,19 +7,17 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
LATEST_VERSION_KEYWORD = '~latest'
- TEMPLATES_DIR = 'templates'
def self.match?(address)
address.include?('@') && address.start_with?(Settings.gitlab_ci['component_fqdn'])
end
- attr_reader :host, :project_file_path
+ attr_reader :host
- def initialize(address:, content_filename:)
+ def initialize(address:)
@full_path, @version = address.to_s.split('@', 2)
- @content_filename = content_filename
@host = Settings.gitlab_ci['component_fqdn']
- @project_file_path = nil
+ @component_project = ::Ci::Catalog::ComponentsProject.new(project, sha)
end
def fetch_content!(current_user:)
@@ -28,7 +26,7 @@ module Gitlab
raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project)
- content(simple_template_path) || content(complex_template_path) || content(legacy_template_path)
+ @component_project.fetch_component(component_name)
end
def project
@@ -46,16 +44,7 @@ module Gitlab
private
- attr_reader :version, :path
-
- def instance_path
- @full_path.delete_prefix(host)
- end
-
- def component_path
- instance_path.delete_prefix(project.full_path).delete_prefix('/')
- end
- strong_memoize_attr :component_path
+ attr_reader :version
# Given a path like "my-org/sub-group/the-project/path/to/component"
# find the project "my-org/sub-group/the-project" by looking at all possible paths.
@@ -65,46 +54,24 @@ module Gitlab
while index = path.rindex('/') # find index of last `/` in a path
possible_paths << (path = path[0..index - 1])
end
-
# remove shortest path as it is group
possible_paths.pop
::Project.where_full_path_in(possible_paths).take # rubocop: disable CodeReuse/ActiveRecord
end
+ def instance_path
+ @full_path.delete_prefix(host)
+ end
+
+ def component_name
+ instance_path.delete_prefix(project.full_path).delete_prefix('/')
+ end
+ strong_memoize_attr :component_name
+
def latest_version_sha
project.releases.latest&.sha
end
-
- # A simple template consists of a single file
- def simple_template_path
- # Extract this line and move to fetch_content once we remove legacy fetching
- return unless templates_dir_exists? && component_path.index('/').nil?
-
- @project_file_path = File.join(TEMPLATES_DIR, "#{component_path}.yml")
- end
-
- # A complex template is directory-based and may consist of multiple files.
- # Given a path like "my-org/sub-group/the-project/templates/component"
- # returns the entry point path: "templates/component/template.yml".
- def complex_template_path
- # Extract this line and move to fetch_content once we remove legacy fetching
- return unless templates_dir_exists? && component_path.index('/').nil?
-
- @project_file_path = File.join(TEMPLATES_DIR, component_path, @content_filename)
- end
-
- def legacy_template_path
- @project_file_path = File.join(component_path, @content_filename).delete_prefix('/')
- end
-
- def templates_dir_exists?
- project.repository.tree.trees.map(&:name).include?(TEMPLATES_DIR)
- end
-
- def content(path)
- project.repository.blob_data_at(sha, path)
- end
end
end
end
diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb
index de6de1bb7a8..03063e76dde 100644
--- a/lib/gitlab/ci/config/external/file/component.rb
+++ b/lib/gitlab/ci/config/external/file/component.rb
@@ -20,7 +20,7 @@ module Gitlab
::Gitlab::UsageDataCounters::HLLRedisCounter.track_event('cicd_component_usage', values: context.user.id)
- component_result.payload.fetch(:content)
+ component_payload.fetch(:content)
end
strong_memoize_attr :content
@@ -65,30 +65,30 @@ module Gitlab
override :expand_context_attrs
def expand_context_attrs
{
- project: component_path.project,
- sha: component_path.sha,
+ project: component_payload.fetch(:project),
+ sha: component_payload.fetch(:sha),
user: context.user,
variables: context.variables
}
end
def masked_blob
- return unless component_path
+ return unless component_payload
context.mask_variables_from(
Gitlab::Routing.url_helpers.project_blob_url(
- component_path.project,
- ::File.join(component_path.sha, component_path.project_file_path))
+ component_payload.fetch(:project),
+ ::File.join(component_payload.fetch(:sha), component_payload.fetch(:path)))
)
end
strong_memoize_attr :masked_blob
- def component_path
+ def component_payload
return unless component_result.success?
- component_result.payload.fetch(:path)
+ component_result.payload
end
- strong_memoize_attr :component_path
+ strong_memoize_attr :component_payload
end
end
end
diff --git a/lib/gitlab/ci/config/yaml/loader.rb b/lib/gitlab/ci/config/yaml/loader.rb
index c659ad5b8d1..1e9ac2b3dd5 100644
--- a/lib/gitlab/ci/config/yaml/loader.rb
+++ b/lib/gitlab/ci/config/yaml/loader.rb
@@ -33,16 +33,16 @@ module Gitlab
end
end
- private
-
- attr_reader :content, :inputs, :variables
-
def load_uninterpolated_yaml
Yaml::Result.new(config: load_yaml!, error: nil)
rescue ::Gitlab::Config::Loader::FormatError => e
Yaml::Result.new(error: e.message, error_class: e)
end
+ private
+
+ attr_reader :content, :inputs, :variables
+
def load_yaml!
ensure_custom_tags
diff --git a/lib/gitlab/ci/config/yaml/result.rb b/lib/gitlab/ci/config/yaml/result.rb
index a68cfde6653..0e7e9230467 100644
--- a/lib/gitlab/ci/config/yaml/result.rb
+++ b/lib/gitlab/ci/config/yaml/result.rb
@@ -39,6 +39,10 @@ module Gitlab
@config.first || {}
end
+
+ def inputs
+ (has_header? && header[:spec][:inputs]) || {}
+ end
end
end
end
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
index 35548358c57..3dc73544208 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb
@@ -15,8 +15,7 @@ module Gitlab
SUPPORTED_SCHEMA_VERSION = '1'
GITLAB_PREFIX = 'gitlab:'
SOURCE_PARSERS = {
- 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning,
- 'container_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning
+ 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning
}.freeze
SUPPORTED_PROPERTIES = %w[
meta:schema_version
@@ -25,10 +24,6 @@ module Gitlab
dependency_scanning:source_file:path
dependency_scanning:package_manager:name
dependency_scanning:language:name
- container_scanning:image:name
- container_scanning:image:tag
- container_scanning:operating_system:name
- container_scanning:operating_system:version
].freeze
def self.parse_source(...)
diff --git a/lib/gitlab/ci/parsers/sbom/source/base_source.rb b/lib/gitlab/ci/parsers/sbom/source/base_source.rb
deleted file mode 100644
index 744555aa25a..00000000000
--- a/lib/gitlab/ci/parsers/sbom/source/base_source.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Parsers
- module Sbom
- module Source
- class BaseSource
- REQUIRED_ATTRIBUTES = [].freeze
-
- def self.source(...)
- new(...).source
- end
-
- def initialize(data)
- @data = data
- end
-
- def source
- return unless required_attributes_present?
-
- ::Gitlab::Ci::Reports::Sbom::Source.new(
- type: type,
- data: data
- )
- end
-
- private
-
- attr_reader :data
-
- # Implement in child class
- # returns a symbol of the source type
- def type; end
-
- def required_attributes_present?
- self.class::REQUIRED_ATTRIBUTES.all? do |keys|
- data.dig(*keys).present?
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
deleted file mode 100644
index eaa3b0db177..00000000000
--- a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Parsers
- module Sbom
- module Source
- class ContainerScanning < BaseSource
- REQUIRED_ATTRIBUTES = [
- %w[image name],
- %w[image tag],
- %w[operating_system name],
- %w[operating_system version]
- ].freeze
-
- private
-
- def type
- :container_scanning
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
index fc5a7606e39..c76a4309779 100644
--- a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
+++ b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb
@@ -5,15 +5,36 @@ module Gitlab
module Parsers
module Sbom
module Source
- class DependencyScanning < BaseSource
+ class DependencyScanning
REQUIRED_ATTRIBUTES = [
%w[input_file path]
].freeze
+ def self.source(...)
+ new(...).source
+ end
+
+ def initialize(data)
+ @data = data
+ end
+
+ def source
+ return unless required_attributes_present?
+
+ ::Gitlab::Ci::Reports::Sbom::Source.new(
+ type: :dependency_scanning,
+ data: data
+ )
+ end
+
private
- def type
- :dependency_scanning
+ attr_reader :data
+
+ def required_attributes_present?
+ REQUIRED_ATTRIBUTES.all? do |keys|
+ data.dig(*keys).present?
+ end
end
end
end
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
index d16f4d7587b..47080ea1979 100644
--- a/lib/gitlab/github_import/bulk_importing.rb
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -32,7 +32,7 @@ module Gitlab
log_error(github_identifiers, build_record.errors.full_messages)
errors << {
validation_errors: build_record.errors,
- github_identifiers: github_identifiers
+ external_identifiers: github_identifiers
}
next
end
@@ -69,7 +69,7 @@ module Gitlab
correlation_id_value: correlation_id_value,
retry_count: nil,
created_at: Time.zone.now,
- external_identifiers: error[:github_identifiers]
+ external_identifiers: error[:external_identifiers]
}
end
@@ -79,8 +79,7 @@ module Gitlab
private
def log_and_increment_counter(value, operation)
- Gitlab::Import::Logger.info(
- import_type: :github,
+ Logger.info(
project_id: project.id,
importer: self.class.name,
message: "#{value} #{object_type.to_s.pluralize} #{operation}"
@@ -95,12 +94,11 @@ module Gitlab
end
def log_error(github_identifiers, messages)
- Gitlab::Import::Logger.error(
- import_type: :github,
+ Logger.error(
project_id: project.id,
importer: self.class.name,
message: messages,
- github_identifiers: github_identifiers
+ external_identifiers: github_identifiers
)
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 23d4faa3dde..5a0ae680ab8 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -284,10 +284,10 @@ module Gitlab
def on_retry
proc do |exception, try, elapsed_time, next_interval|
- Gitlab::Import::Logger.info(
+ Logger.info(
message: "GitHub connection retry triggered",
'error.class': exception.class,
- 'error.message': exception.message,
+ 'exception.message': exception.message,
try_count: try,
elapsed_time_s: elapsed_time,
wait_to_retry_s: next_interval
diff --git a/lib/gitlab/github_import/importer/attachments/issues_importer.rb b/lib/gitlab/github_import/importer/attachments/issues_importer.rb
index c8f0b59fd18..a0e1a3f2d25 100644
--- a/lib/gitlab/github_import/importer/attachments/issues_importer.rb
+++ b/lib/gitlab/github_import/importer/attachments/issues_importer.rb
@@ -24,7 +24,7 @@ module Gitlab
private
def collection
- project.issues.select(:id, :description, :iid)
+ project.issues.id_not_in(already_imported_ids).select(:id, :description, :iid)
end
def ordering_column
diff --git a/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb b/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb
index cd3a327a846..22b3e7c640b 100644
--- a/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb
+++ b/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb
@@ -24,7 +24,7 @@ module Gitlab
private
def collection
- project.merge_requests.select(:id, :description, :iid)
+ project.merge_requests.id_not_in(already_imported_ids).select(:id, :description, :iid)
end
def ordering_column
diff --git a/lib/gitlab/github_import/importer/attachments/notes_importer.rb b/lib/gitlab/github_import/importer/attachments/notes_importer.rb
index aa38a7a3a3f..5ab0cf5b6b0 100644
--- a/lib/gitlab/github_import/importer/attachments/notes_importer.rb
+++ b/lib/gitlab/github_import/importer/attachments/notes_importer.rb
@@ -26,7 +26,7 @@ module Gitlab
# TODO: exclude :system, :noteable_type from select after removing override Note#note method
# https://gitlab.com/gitlab-org/gitlab/-/issues/369923
def collection
- project.notes.user.select(:id, :note, :system, :noteable_type)
+ project.notes.id_not_in(already_imported_ids).user.select(:id, :note, :system, :noteable_type)
end
end
end
diff --git a/lib/gitlab/github_import/importer/attachments/releases_importer.rb b/lib/gitlab/github_import/importer/attachments/releases_importer.rb
index 7d6dbeb901e..0527170f5e1 100644
--- a/lib/gitlab/github_import/importer/attachments/releases_importer.rb
+++ b/lib/gitlab/github_import/importer/attachments/releases_importer.rb
@@ -24,7 +24,7 @@ module Gitlab
private
def collection
- project.releases.select(:id, :description, :tag)
+ project.releases.id_not_in(already_imported_ids).select(:id, :description, :tag)
end
end
end
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
index c2ea7ba4874..d49180e6927 100644
--- a/lib/gitlab/github_import/importer/diff_note_importer.rb
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -126,7 +126,7 @@ module Gitlab
Logger.info(
project_id: project.id,
importer: self.class.name,
- github_identifiers: note.github_identifiers,
+ external_identifiers: note.github_identifiers,
model: model
)
end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
index 62863ba67fd..671e023e90b 100644
--- a/lib/gitlab/github_import/importer/pull_requests_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -44,7 +44,7 @@ module Gitlab
pname = project.path_with_namespace
- Gitlab::Import::Logger.info(
+ Logger.info(
message: 'GitHub importer finished updating repository',
project_name: pname
)
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index cfc1ec526b0..cccd99f48b1 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -211,6 +211,12 @@ module Gitlab
private
+ # Returns the set used to track "already imported" objects.
+ # Items are the values returned by `#id_for_already_imported_cache`.
+ def already_imported_ids
+ Gitlab::Cache::Import::Caching.values_from_set(already_imported_cache_key)
+ end
+
def additional_object_data
{}
end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index 1832f071a44..4bf2d8a0aca 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -271,8 +271,7 @@ module Gitlab
end
def log(message, username: nil)
- Gitlab::Import::Logger.info(
- import_type: :github,
+ Logger.info(
project_id: project.id,
class: self.class.name,
username: username,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d5fad34dd4e..d5671460bbb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8891,9 +8891,6 @@ msgstr ""
msgid "BulkImport|Direct transfer maximum download file size (MiB)"
msgstr ""
-msgid "BulkImport|Existing groups"
-msgstr ""
-
msgid "BulkImport|Filter by source group"
msgstr ""
@@ -22623,12 +22620,6 @@ msgstr ""
msgid "Group membership expiration date removed"
msgstr ""
-msgid "Group mention in private"
-msgstr ""
-
-msgid "Group mention in public"
-msgstr ""
-
msgid "Group milestone"
msgstr ""
@@ -25268,6 +25259,12 @@ msgstr ""
msgid "IntegrationEvents|A deployment is started or finished"
msgstr ""
+msgid "IntegrationEvents|A group is mentioned in a confidential context"
+msgstr ""
+
+msgid "IntegrationEvents|A group is mentioned in a public context"
+msgstr ""
+
msgid "IntegrationEvents|A merge request is created, merged, closed, or reopened"
msgstr ""
@@ -30434,12 +30431,20 @@ msgstr ""
msgid "MlExperimentTracking|Triggered by"
msgstr ""
+msgid "MlModelRegistry|%{version} · No other versions"
+msgid_plural "MlModelRegistry|%{version} · %{versionCount} versions"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "MlModelRegistry|Model registry"
msgstr ""
msgid "MlModelRegistry|No models registered in this project"
msgstr ""
+msgid "MlModelRegistry|No registered versions"
+msgstr ""
+
msgid "Modal updated"
msgstr ""
@@ -33074,9 +33079,15 @@ msgstr ""
msgid "Organizations"
msgstr ""
+msgid "Organization|%{linkStart}Organizations%{linkEnd} are a top-level container to hold your groups and projects."
+msgstr ""
+
msgid "Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder."
msgstr ""
+msgid "Organization|An error occurred creating an organization. Please try again."
+msgstr ""
+
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
msgstr ""
@@ -33092,6 +33103,9 @@ msgstr ""
msgid "Organization|Create an organization to contain all of your groups and projects."
msgstr ""
+msgid "Organization|Create organization"
+msgstr ""
+
msgid "Organization|Frequently visited groups"
msgstr ""
@@ -33104,12 +33118,30 @@ msgstr ""
msgid "Organization|Manage"
msgstr ""
+msgid "Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses."
+msgstr ""
+
+msgid "Organization|My organization"
+msgstr ""
+
msgid "Organization|New organization"
msgstr ""
msgid "Organization|Org ID"
msgstr ""
+msgid "Organization|Organization URL"
+msgstr ""
+
+msgid "Organization|Organization URL is required."
+msgstr ""
+
+msgid "Organization|Organization name"
+msgstr ""
+
+msgid "Organization|Organization name is required."
+msgstr ""
+
msgid "Organization|Organization navigation"
msgstr ""
@@ -33134,6 +33166,9 @@ msgstr ""
msgid "Organization|You don't have any projects yet."
msgstr ""
+msgid "Organization|my-organization"
+msgstr ""
+
msgid "Orphaned member"
msgstr ""
@@ -37021,12 +37056,6 @@ msgstr ""
msgid "ProjectService|Trigger event when a deployment starts or finishes."
msgstr ""
-msgid "ProjectService|Trigger event when a group is mentioned in a confidential context."
-msgstr ""
-
-msgid "ProjectService|Trigger event when a group is mentioned in a public context."
-msgstr ""
-
msgid "ProjectService|Trigger event when a merge request is created, updated, or merged."
msgstr ""
diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb
index 9fb262c27c3..52e5593cb26 100644
--- a/qa/qa/page/group/bulk_import.rb
+++ b/qa/qa/page/group/bulk_import.rb
@@ -11,12 +11,8 @@ module QA
element 'filter-groups'
end
- view "app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue" do
- element :target_group_dropdown_item
- end
-
- view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
- element :target_namespace_selector_dropdown
+ view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do
+ element 'target-namespace-dropdown'
end
view "app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue" do
@@ -40,8 +36,8 @@ module QA
filter_group(source_group_name)
within_element(:import_item, source_group: source_group_name) do
- click_element(:target_namespace_selector_dropdown)
- click_element(:target_group_dropdown_item, group_name: target_group_name)
+ click_element('target-namespace-dropdown')
+ click_element("listbox-item-#{target_group_name}")
retry_until(message: "Triggering import") do
click_element('import-group-button')
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index 9e1bf0a393f..08400042028 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -20,7 +20,7 @@ module QA
end
view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do
- element :target_namespace_selector_dropdown
+ element 'target-namespace-dropdown'
end
# Add personal access token
@@ -46,7 +46,7 @@ module QA
# @return [void]
def import!(gh_project_name, target_group_path, project_name)
within_element(:project_import_row, source_project: gh_project_name) do
- click_element(:target_namespace_selector_dropdown)
+ click_element('target-namespace-dropdown')
click_element("listbox-item-#{target_group_path}", wait: 10)
fill_element(:project_path_field, project_name)
diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb
index 586190c8fc0..c42c94d5d01 100644
--- a/spec/components/projects/ml/models_index_component_spec.rb
+++ b/spec/components/projects/ml/models_index_component_spec.rb
@@ -30,6 +30,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
let(:element) { page.find("#js-index-ml-models") }
before do
+ allow(model1).to receive(:version_count).and_return(1)
+ allow(model2).to receive(:version_count).and_return(0)
render_inline component
end
@@ -41,12 +43,14 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat
{
'name' => model1.name,
'version' => model1.latest_version.version,
- 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}"
+ 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}",
+ 'versionCount' => 1
},
{
'name' => model2.name,
'version' => nil,
- 'path' => nil
+ 'path' => nil,
+ 'versionCount' => 0
}
],
'pageInfo' => {
diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb
index 3205b46c5b2..489470dc0df 100644
--- a/spec/controllers/oauth/tokens_controller_spec.rb
+++ b/spec/controllers/oauth/tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::TokensController do
+RSpec.describe Oauth::TokensController, feature_category: :user_management do
let(:user) { create(:user) }
it 'includes Two-factor enforcement concern' do
@@ -24,12 +24,6 @@ RSpec.describe Oauth::TokensController do
end
end
- before do
- Rails.application.routes.draw do
- post 'create' => 'anonymous#create'
- end
- end
-
it 'does log correlation id' do
Labkit::Correlation::CorrelationId.use_id('new-id') do
post :create
diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb
index 94bcec6163e..f81e940c7ed 100644
--- a/spec/finders/concerns/packages/finder_helper_spec.rb
+++ b/spec/finders/concerns/packages/finder_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::FinderHelper do
+RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do
describe '#packages_for_project' do
let_it_be_with_reload(:project1) { create(:project) }
let_it_be(:package1) { create(:package, project: project1) }
@@ -107,6 +107,34 @@ RSpec.describe ::Packages::FinderHelper do
it_behaves_like params[:shared_example_name]
end
+
+ context 'when the second project has the package registry disabled' do
+ before do
+ project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ package_registry_access_level: 'disabled', packages_enabled: false)
+ end
+
+ it_behaves_like 'returning both packages'
+
+ context 'with with_package_registry_enabled set to true' do
+ let(:finder_class) do
+ Class.new do
+ include ::Packages::FinderHelper
+
+ def initialize(user)
+ @current_user = user
+ end
+
+ def execute(group)
+ packages_visible_to_user(@current_user, within_group: group, with_package_registry_enabled: true)
+ end
+ end
+ end
+
+ it_behaves_like 'returning package1'
+ end
+ end
end
context 'with a group deploy token' do
diff --git a/spec/finders/packages/npm/packages_for_user_finder_spec.rb b/spec/finders/packages/npm/packages_for_user_finder_spec.rb
index e2dc21e1008..ffbb4f9e484 100644
--- a/spec/finders/packages/npm/packages_for_user_finder_spec.rb
+++ b/spec/finders/packages/npm/packages_for_user_finder_spec.rb
@@ -36,6 +36,24 @@ RSpec.describe ::Packages::Npm::PackagesForUserFinder, feature_category: :packag
end
it_behaves_like 'searches for packages'
+
+ context 'when an user is a reporter of both projects' do
+ before_all do
+ project2.add_reporter(user)
+ end
+
+ it { is_expected.to contain_exactly(package, package_with_diff_project) }
+
+ context 'when the second project has the package registry disabled' do
+ before_all do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ package_registry_access_level: 'disabled', packages_enabled: false)
+ end
+
+ it_behaves_like 'searches for packages'
+ end
+ end
end
end
end
diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb
index 48333ae49e5..1d869e1792d 100644
--- a/spec/finders/projects/ml/model_finder_spec.rb
+++ b/spec/finders/projects/ml/model_finder_spec.rb
@@ -22,4 +22,8 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do
it 'does not return models belonging to a different project' do
is_expected.not_to include(model3)
end
+
+ it 'includes version count' do
+ expect(models[0].version_count).to be(models[0].versions.count)
+ end
end
diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js
index fabf43ceb9d..083b49b7c30 100644
--- a/spec/frontend/crm/crm_form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -10,7 +10,7 @@ import routes from '~/crm/contacts/routes';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
import {
createContactMutationErrorResponse,
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index 8408c1920a9..f15fcac71d5 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -2,7 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
import CrmForm from '~/crm/components/crm_form.vue';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
describe('Customer relations organization form wrapper', () => {
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
index 9788b9062d6..ed15c66f4c6 100644
--- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -97,7 +97,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should request namespaced services from the cluster_client library if namespace is specified', async () => {
const services = await mockResolvers.Query.k8sServices(null, { configuration, namespace });
- expect(mockNamespacedServicesListFn).toHaveBeenCalledWith(namespace);
+ expect(mockNamespacedServicesListFn).toHaveBeenCalledWith({ namespace });
expect(mockAllServicesListFn).not.toHaveBeenCalled();
expect(services).toEqual(k8sServicesMock);
diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js
index 67148173404..b61a7f36f85 100644
--- a/spec/frontend/import/details/mock_data.js
+++ b/spec/frontend/import/details/mock_data.js
@@ -7,7 +7,7 @@ export const mockImportFailures = [
exception_class: 'ActiveRecord::RecordInvalid',
exception_message: 'Record invalid',
source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
- github_identifiers: {
+ external_identifiers: {
iid: 2,
issuable_type: 'MergeRequest',
object_type: 'pull_request',
@@ -22,7 +22,7 @@ export const mockImportFailures = [
exception_class: 'ActiveRecord::RecordInvalid',
exception_message: 'Record invalid',
source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
- github_identifiers: {
+ external_identifiers: {
iid: 3,
issuable_type: 'MergeRequest',
object_type: 'pull_request',
@@ -37,7 +37,7 @@ export const mockImportFailures = [
exception_class: 'NameError',
exception_message: 'some message',
source: 'Gitlab::GithubImport::Importer::LfsObjectImporter',
- github_identifiers: {
+ external_identifiers: {
oid: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb',
size: 2473979,
},
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
deleted file mode 100644
index 14f39a35387..00000000000
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
-
-Vue.use(VueApollo);
-
-const makeGroupMock = (fullPath) => ({
- id: `gid://gitlab/Group/${fullPath}`,
- fullPath,
- name: fullPath,
- visibility: 'public',
- webUrl: `http://gdk.test:3000/groups/${fullPath}`,
- __typename: 'Group',
-});
-
-const AVAILABLE_NAMESPACES = [
- makeGroupMock('match1'),
- makeGroupMock('unrelated'),
- makeGroupMock('match2'),
-];
-
-const SEARCH_NAMESPACES_MOCK = Promise.resolve({
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- groups: {
- nodes: AVAILABLE_NAMESPACES,
- __typename: 'GroupConnection',
- },
- namespace: {
- id: 'gid://gitlab/Namespaces::UserNamespace/1',
- fullPath: 'root',
- __typename: 'Namespace',
- },
- __typename: 'UserCore',
- },
- },
-});
-
-describe('Import entities group dropdown component', () => {
- let wrapper;
- let namespacesTracker;
-
- const createComponent = (propsData) => {
- const apolloProvider = createMockApollo([
- [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
- ]);
-
- namespacesTracker = jest.fn();
-
- wrapper = shallowMount(GroupDropdown, {
- apolloProvider,
- scopedSlots: {
- default: namespacesTracker,
- },
- stubs: { GlDropdown },
- propsData,
- });
- };
-
- it('passes namespaces from graphql query to default slot', async () => {
- createComponent();
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await nextTick();
- await waitForPromises();
- await nextTick();
-
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES });
- });
-
- it('filters namespaces based on user input', async () => {
- createComponent();
-
- namespacesTracker.mockReset();
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await nextTick();
- await waitForPromises();
- await nextTick();
-
- expect(namespacesTracker).toHaveBeenCalledWith({
- namespaces: [
- expect.objectContaining({ fullPath: 'match1' }),
- expect.objectContaining({ fullPath: 'match2' }),
- ],
- });
- });
-});
diff --git a/spec/frontend/import_entities/components/import_target_dropdown_spec.js b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
index c12baed2374..ba0bb0b0f74 100644
--- a/spec/frontend/import_entities/components/import_target_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
@@ -18,7 +18,6 @@ describe('ImportTargetDropdown', () => {
const defaultProps = {
selected: mockUserNamespace,
- userNamespace: mockUserNamespace,
};
const createComponent = ({ props = {} } = {}) => {
@@ -39,7 +38,7 @@ describe('ImportTargetDropdown', () => {
};
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const findListboxUsersItems = () => findListbox().props('items')[0].options;
+ const findListboxFirstGroupItems = () => findListbox().props('items')[0].options;
const findListboxGroupsItems = () => findListbox().props('items')[1].options;
const waitForQuery = async () => {
@@ -63,12 +62,54 @@ describe('ImportTargetDropdown', () => {
expect(findListbox().props('toggleText')).toBe('a-group-path-that-is-lo…');
});
- it('passes userNamespace as "Users" group item', () => {
- createComponent();
+ describe('when used on group import', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findListboxUsersItems()).toEqual([
- { text: mockUserNamespace, value: mockUserNamespace },
- ]);
+ it('adds "No parent" in "Parent" group', () => {
+ expect(findListboxFirstGroupItems()).toEqual([{ text: 'No parent', value: '' }]);
+ });
+
+ it('emits "select" event with { fullPath: "", id: null } when "No parent" is selected', () => {
+ findListbox().vm.$emit('select', '');
+
+ expect(wrapper.emitted('select')[0]).toEqual([{ fullPath: '', id: null }]);
+ });
+
+ it('emits "select" event with { fullPath, id } when a group is selected', async () => {
+ await waitForQuery();
+
+ const mockGroupPath = 'match1';
+
+ findListbox().vm.$emit('select', mockGroupPath);
+
+ expect(wrapper.emitted('select')[0]).toEqual([
+ { fullPath: mockGroupPath, id: `gid://gitlab/Group/${mockGroupPath}` },
+ ]);
+ });
+ });
+
+ describe('when used on project import', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { userNamespace: mockUserNamespace },
+ });
+ });
+
+ it('passes userNamespace as "Users" group item', () => {
+ expect(findListboxFirstGroupItems()).toEqual([
+ { text: mockUserNamespace, value: mockUserNamespace },
+ ]);
+ });
+
+ it('emits "select" event with path as value', () => {
+ const mockProjectPath = 'mock-project';
+
+ findListbox().vm.$emit('select', mockProjectPath);
+
+ expect(wrapper.emitted('select')[0]).toEqual([mockProjectPath]);
+ });
});
it('passes namespaces from GraphQL as "Groups" group item', async () => {
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 68fbd8dae18..4fab22e316a 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -57,7 +57,7 @@ describe('import table', () => {
];
const findPaginationDropdown = () => wrapper.findByTestId('page-size');
const findTargetNamespaceDropdown = (rowWrapper) =>
- extendedWrapper(rowWrapper).findByTestId('target-namespace-selector');
+ extendedWrapper(rowWrapper).findByTestId('target-namespace-dropdown');
const findTargetNamespaceInput = (rowWrapper) =>
extendedWrapper(rowWrapper).findByTestId('target-namespace-input');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index 46884a42707..ac95026a9a4 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -1,10 +1,9 @@
-import { GlDropdownItem, GlFormInput } from '@gitlab/ui';
+import { GlFormInput } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
+import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -37,7 +36,7 @@ describe('import target cell', () => {
let group;
const findNameInput = () => wrapper.findComponent(GlFormInput);
- const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+ const findNamespaceDropdown = () => wrapper.findComponent(ImportTargetDropdown);
const createComponent = (props) => {
apolloProvider = createMockApollo([
@@ -49,7 +48,7 @@ describe('import target cell', () => {
wrapper = shallowMount(ImportTargetCell, {
apolloProvider,
- stubs: { ImportGroupDropdown },
+ stubs: { ImportTargetDropdown },
propsData: {
groupPathRegex: /.*/,
...props,
@@ -73,14 +72,14 @@ describe('import target cell', () => {
});
it('emits update-target-namespace when dropdown option is clicked', () => {
- const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2);
+ const targetNamespace = {
+ fullPath: AVAILABLE_NAMESPACES[1].fullPath,
+ id: AVAILABLE_NAMESPACES[1].id,
+ };
- dropdownItem.vm.$emit('click');
+ findNamespaceDropdown().vm.$emit('select', targetNamespace);
- expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual(
- AVAILABLE_NAMESPACES[1],
- );
+ expect(wrapper.emitted('update-target-namespace')[0]).toStrictEqual([targetNamespace]);
});
});
@@ -101,36 +100,6 @@ describe('import target cell', () => {
});
});
- it('renders only no parent option if available namespaces list is empty', () => {
- createComponent({
- group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- availableNamespaces: [],
- });
-
- const items = findNamespaceDropdown()
- .findAllComponents(GlDropdownItem)
- .wrappers.map((w) => w.text());
-
- expect(items[0]).toBe('No parent');
- expect(items).toHaveLength(1);
- });
-
- it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => {
- createComponent({
- group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- });
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await waitForPromises();
- await nextTick();
-
- const [firstItem, ...rest] = findNamespaceDropdown()
- .findAllComponents(GlDropdownItem)
- .wrappers.map((w) => w.text());
-
- expect(firstItem).toBe('No parent');
- expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length);
- });
-
describe('when entity is not available for import', () => {
beforeEach(() => {
group = generateFakeTableEntry({
@@ -147,6 +116,7 @@ describe('import target cell', () => {
describe('when entity is available for import', () => {
const FAKE_PROGRESS_MESSAGE = 'progress message';
+
beforeEach(() => {
group = generateFakeTableEntry({
id: 1,
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
index b4aad5ca292..c1b9aef9634 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
@@ -1,6 +1,6 @@
-import { GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
+import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import { mockModels, startCursor, defaultPageInfo } from './mock_data';
@@ -10,10 +10,8 @@ const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageIn
wrapper = shallowMountExtended(MlModelsIndexApp, { propsData });
};
-const findModelLink = (index) => wrapper.findAllComponents(GlLink).at(index);
+const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
const findPagination = () => wrapper.findComponent(Pagination);
-const modelLinkText = (index) => findModelLink(index).text();
-const modelLinkHref = (index) => findModelLink(index).attributes('href');
const findTitle = () => wrapper.findByText(TITLE_LABEL);
const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
@@ -47,11 +45,8 @@ describe('MlModelsIndex', () => {
describe('model list', () => {
it('displays the models', () => {
- expect(modelLinkHref(0)).toBe(mockModels[0].path);
- expect(modelLinkText(0)).toBe(`${mockModels[0].name} / ${mockModels[0].version}`);
-
- expect(modelLinkHref(1)).toBe(mockModels[1].path);
- expect(modelLinkText(1)).toBe(`${mockModels[1].name} / ${mockModels[1].version}`);
+ expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);
+ expect(findModelRow(1).props('model')).toMatchObject(mockModels[1]);
});
});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
index e65552353c3..841a543606f 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
@@ -3,14 +3,22 @@ export const mockModels = [
name: 'model_1',
version: '1.0',
path: 'path/to/model_1',
+ versionCount: 3,
},
{
name: 'model_2',
- version: '1.0',
+ version: '1.1',
path: 'path/to/model_2',
+ versionCount: 1,
},
];
+export const modelWithoutVersion = {
+ name: 'model_without_version',
+ path: 'path/to/model_without_version',
+ versionCount: 0,
+};
+
export const startCursor = 'eyJpZCI6IjE2In0';
export const defaultPageInfo = Object.freeze({
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
new file mode 100644
index 00000000000..7600288f560
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
@@ -0,0 +1,42 @@
+import { GlLink } from '@gitlab/ui';
+import {
+ mockModels,
+ modelWithoutVersion,
+} from 'jest/ml/model_registry/routes/models/index/components/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
+
+let wrapper;
+const createWrapper = (model = mockModels[0]) => {
+ wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
+};
+
+const findLink = () => wrapper.findComponent(GlLink);
+const findMessage = (message) => wrapper.findByText(message);
+
+describe('ModelRow', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('Has a link to the model', () => {
+ expect(findLink().text()).toBe(mockModels[0].name);
+ expect(findLink().attributes('href')).toBe(mockModels[0].path);
+ });
+
+ it('Shows the latest version and the version count', () => {
+ expect(findMessage('1.0 · 3 versions').exists()).toBe(true);
+ });
+
+ it('Shows the latest version and no version count if it has only 1 version', () => {
+ createWrapper(mockModels[1]);
+
+ expect(findMessage('1.1 · No other versions').exists()).toBe(true);
+ });
+
+ it('Shows no version message if model has no versions', () => {
+ createWrapper(modelWithoutVersion);
+
+ expect(findMessage('No registered versions').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js
index 34b7524d8fb..620c753e3c5 100644
--- a/spec/frontend/notes/components/email_participants_warning_spec.js
+++ b/spec/frontend/notes/components/email_participants_warning_spec.js
@@ -1,10 +1,12 @@
import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+
import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
describe('Email Participants Warning Component', () => {
let wrapper;
- const findMoreButton = () => wrapper.find('button');
+ const findMoreButton = () => wrapper.findComponent(GlButton);
const createWrapper = (emails) => {
wrapper = mount(EmailParticipantsWarning, {
@@ -48,7 +50,7 @@ describe('Email Participants Warning Component', () => {
describe('when more button clicked', () => {
beforeEach(() => {
- findMoreButton().trigger('click');
+ findMoreButton().vm.$emit('click');
});
it('more button no longer exists', () => {
diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js
new file mode 100644
index 00000000000..2206a6f0384
--- /dev/null
+++ b/spec/frontend/organizations/new/components/app_spec.js
@@ -0,0 +1,103 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import App from '~/organizations/new/components/app.vue';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createOrganizationResponse } from '~/organizations/mock_data';
+import { createAlert } from '~/alert';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/alert');
+
+describe('OrganizationNewApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
+ };
+
+ const findForm = () => wrapper.findComponent(NewEditForm);
+ const submitForm = async () => {
+ findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
+ await nextTick();
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders form', () => {
+ createComponent();
+
+ expect(findForm().exists()).toBe(true);
+ });
+
+ describe('when form is submitted', () => {
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+
+ await submitForm();
+ });
+
+ it('sets `NewEditForm` `loading` prop to `true`', () => {
+ expect(findForm().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('redirects user to organization path', () => {
+ expect(visitUrl).toHaveBeenCalledWith(createOrganizationResponse.organization.path);
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ createOrganization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred creating an organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
new file mode 100644
index 00000000000..43c099fbb1c
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -0,0 +1,112 @@
+import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('NewEditForm', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const defaultPropsData = {
+ loading: false,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(NewEditForm, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findNameField = () => wrapper.findByLabelText('Organization name');
+ const findUrlField = () => wrapper.findByLabelText('Organization URL');
+ const submitForm = async () => {
+ await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
+ };
+
+ it('renders `Organization name` field', () => {
+ createComponent();
+
+ expect(findNameField().exists()).toBe(true);
+ });
+
+ it('renders `Organization URL` field', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlInputGroupText).findComponent(GlTruncate).props('text')).toBe(
+ 'http://127.0.0.1:3000/-/organizations/',
+ );
+ expect(findUrlField().exists()).toBe(true);
+ });
+
+ describe('when form is submitted without filling in required fields', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ });
+
+ it('shows error messages', () => {
+ expect(wrapper.findByText('Organization name is required.').exists()).toBe(true);
+ expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true);
+ });
+ });
+
+ describe('when form is submitted successfully', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findNameField().setValue('Foo bar');
+ await findUrlField().setValue('foo-bar');
+ await submitForm();
+ });
+
+ it('emits `submit` event with form values', () => {
+ expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]);
+ });
+ });
+
+ describe('when `Organization URL` has not been manually set', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findNameField().setValue('Foo bar');
+ await submitForm();
+ });
+
+ it('sets `Organization URL` when typing in `Organization name`', () => {
+ expect(findUrlField().element.value).toBe('foo-bar');
+ });
+ });
+
+ describe('when `Organization URL` has been manually set', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findUrlField().setValue('foo-bar-baz');
+ await findNameField().setValue('Foo bar');
+ await submitForm();
+ });
+
+ it('does not modify `Organization URL` when typing in `Organization name`', () => {
+ expect(findUrlField().element.value).toBe('foo-bar-baz');
+ });
+ });
+
+ describe('when `loading` prop is `true`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { loading: true } });
+ });
+
+ it('shows button with loading icon', () => {
+ expect(wrapper.findComponent(GlButton).props('loading')).toBe(true);
+ });
+ });
+});
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index 67aa57dcad7..cf8ae358e49 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
let_it_be(:new_group_path) { '/groups/new' }
let_it_be(:new_project_path) { '/projects/new' }
let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' }
+ let_it_be(:organizations_path) { '/-/organizations/' }
+ let_it_be(:root_url) { 'http://127.0.0.1:3000/' }
let_it_be(:groups_empty_state_svg_path) { 'illustrations/empty-state/empty-groups-md.svg' }
let_it_be(:projects_empty_state_svg_path) { 'illustrations/empty-state/empty-projects-md.svg' }
@@ -15,6 +17,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
allow(helper).to receive(:new_project_path).and_return(new_project_path)
allow(helper).to receive(:image_path).with(organizations_empty_state_svg_path)
.and_return(organizations_empty_state_svg_path)
+ allow(helper).to receive(:organizations_path).and_return(organizations_path)
+ allow(helper).to receive(:root_url).and_return(root_url)
allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path)
allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path)
end
@@ -76,4 +80,15 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
)
end
end
+
+ describe '#organization_new_app_data' do
+ it 'returns expected json' do
+ expect(Gitlab::Json.parse(helper.organization_new_app_data)).to eq(
+ {
+ 'organizations_path' => organizations_path,
+ 'root_url' => root_url
+ }
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
index 97843781891..0bdcfcfd546 100644
--- a/spec/lib/gitlab/ci/components/instance_path_spec.rb
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_composition do
let_it_be(:user) { create(:user) }
- let(:path) { described_class.new(address: address, content_filename: 'template.yml') }
+ let(:path) { described_class.new(address: address) }
let(:settings) { GitlabSettings::Options.build({ 'component_fqdn' => current_host }) }
let(:current_host) { 'acme.com/' }
@@ -44,9 +44,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
context 'when the component is simple (single file template)' do
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine_1')
+ expect(result.path).to eq('templates/secret-detection.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('templates/secret-detection.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -56,9 +57,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
let(:address) { "acme.com/#{project_path}/dast@#{version}" }
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine_2')
+ expect(result.path).to eq('templates/dast/template.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('templates/dast/template.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -67,7 +69,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" }
it 'returns nil' do
- expect(path.fetch_content!(current_user: user)).to be_nil
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to be_nil
end
end
@@ -75,7 +78,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" }
it 'returns nil' do
- expect(path.fetch_content!(current_user: user)).to be_nil
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to be_nil
end
end
end
@@ -110,9 +114,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
end
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine_2')
+ expect(result.path).to eq('templates/secret-detection.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('templates/secret-detection.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(latest_sha)
end
@@ -124,7 +129,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
it 'returns nil', :aggregate_failures do
expect(path.fetch_content!(current_user: user)).to be_nil
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to be_nil
expect(path.project).to eq(project)
expect(path.sha).to be_nil
end
@@ -135,9 +139,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
let(:current_host) { 'acme.com/gitlab/' }
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine_1')
+ expect(result.path).to eq('templates/secret-detection.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('templates/secret-detection.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -164,9 +169,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
end
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine')
+ expect(result.path).to eq('component/template.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('component/template.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -184,9 +190,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
end
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine')
+ expect(result.path).to eq('component/template.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('component/template.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -197,9 +204,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
let(:current_host) { 'acme.com/gitlab/' }
it 'fetches the component content', :aggregate_failures do
- expect(path.fetch_content!(current_user: user)).to eq('image: alpine')
+ result = path.fetch_content!(current_user: user)
+ expect(result.content).to eq('image: alpine')
+ expect(result.path).to eq('component/template.yml')
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to eq('component/template.yml')
expect(path.project).to eq(project)
expect(path.sha).to eq(project.commit('master').id)
end
@@ -211,7 +219,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline
it 'returns nil', :aggregate_failures do
expect(path.fetch_content!(current_user: user)).to be_nil
expect(path.host).to eq(current_host)
- expect(path.project_file_path).to be_nil
expect(path.project).to eq(project)
expect(path.sha).to be_nil
end
diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
index 0f7b811b5df..88e272ac3fd 100644
--- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -99,7 +99,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
let(:response) do
ServiceResponse.success(payload: {
content: content,
- path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345')
+ path: 'templates/component.yml',
+ project: project,
+ sha: '12345'
})
end
@@ -132,7 +134,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
let(:response) do
ServiceResponse.success(payload: {
content: content,
- path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345')
+ path: 'templates/component.yml',
+ project: project,
+ sha: '12345'
})
end
@@ -158,15 +162,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
describe '#metadata' do
subject(:metadata) { external_resource.metadata }
- let(:component_path) do
- instance_double(::Gitlab::Ci::Components::InstancePath,
- project: project,
- sha: '12345',
- project_file_path: 'my-component/template.yml')
- end
-
let(:response) do
- ServiceResponse.success(payload: { path: component_path })
+ ServiceResponse.success(payload: { path: 'my-component/template.yml', project: project, sha: '12345' })
end
it 'returns the metadata' do
@@ -183,14 +180,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
end
describe '#expand_context' do
- let(:component_path) do
- instance_double(::Gitlab::Ci::Components::InstancePath,
- project: project,
- sha: '12345')
- end
-
let(:response) do
- ServiceResponse.success(payload: { path: component_path })
+ ServiceResponse.success(payload: { path: 'templates/component.yml', project: project, sha: '12345' })
end
subject { external_resource.send(:expand_context_attrs) }
@@ -207,11 +198,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
describe '#to_hash' do
context 'when interpolation is being used' do
let(:response) do
- ServiceResponse.success(payload: { content: content, path: path })
- end
-
- let(:path) do
- instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345')
+ ServiceResponse.success(payload: { content: content, path: 'templates/component.yml', project: project,
+ sha: '12345' })
end
let(:content) do
diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
index 57a9a47d699..684da1df43b 100644
--- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
@@ -58,4 +58,36 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c
end
end
end
+
+ describe '#load_uninterpolated_yaml' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ test_job:
+ script:
+ - echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ subject(:result) { described_class.new(yaml).load_uninterpolated_yaml }
+
+ it 'returns the config' do
+ expected_content = { test_job: { script: ["echo \"$[[ inputs.test_input ]]\""] } }
+ expect(result).to be_valid
+ expect(result.content).to eq(expected_content)
+ end
+
+ context 'when there is a format error in the yaml' do
+ let(:yaml) { 'invalid: yaml: all the time' }
+
+ it 'returns an error' do
+ expect(result).not_to be_valid
+ expect(result.error).to include('mapping values are not allowed in this context')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
index a66c630dfc9..5e9dee02190 100644
--- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
@@ -3,12 +3,44 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_composition do
+ it 'raises an error when reading a header when there is none' do
+ result = described_class.new(config: { b: 2 })
+
+ expect { result.header }.to raise_error(ArgumentError)
+ end
+
+ it 'stores an error / exception when initialized with it' do
+ result = described_class.new(error: ArgumentError.new('abc'))
+
+ expect(result).not_to be_valid
+ expect(result.error).to be_a ArgumentError
+ end
+
it 'does not have a header when config is a single hash' do
result = described_class.new(config: { a: 1, b: 2 })
expect(result).not_to have_header
end
+ describe '#inputs' do
+ it 'returns the value of the spec inputs' do
+ result = described_class.new(config: [{ spec: { inputs: { website: nil } } }, { b: 2 }])
+
+ expect(result).to have_header
+ expect(result.inputs).to eq({ website: nil })
+ end
+ end
+
+ describe '#interpolated?' do
+ it 'defaults to false' do
+ expect(described_class.new).not_to be_interpolated
+ end
+
+ it 'returns the value passed to the initializer' do
+ expect(described_class.new(interpolated: true)).to be_interpolated
+ end
+ end
+
context 'when config is an array of hashes' do
context 'when first document matches the header schema' do
it 'has a header' do
@@ -38,27 +70,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com
expect(result.content).to be_empty
end
end
-
- it 'raises an error when reading a header when there is none' do
- result = described_class.new(config: { b: 2 })
-
- expect { result.header }.to raise_error(ArgumentError)
- end
-
- it 'stores an error / exception when initialized with it' do
- result = described_class.new(error: ArgumentError.new('abc'))
-
- expect(result).not_to be_valid
- expect(result.error).to be_a ArgumentError
- end
-
- describe '#interpolated?' do
- it 'defaults to false' do
- expect(described_class.new).not_to be_interpolated
- end
-
- it 'returns the value passed to the initializer' do
- expect(described_class.new(interpolated: true)).to be_interpolated
- end
- end
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
index 2c57106b07c..dacbe07c8b3 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -42,16 +42,15 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
it { is_expected.to be_nil }
end
- context 'when no dependency_scanning or container_scanning properties are present' do
+ context 'when no dependency_scanning properties are present' do
let(:properties) do
[
{ 'name' => 'gitlab:meta:schema_version', 'value' => '1' }
]
end
- it 'does not call source parsers' do
+ it 'does not call dependency_scanning parser' do
expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:source)
- expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).not_to receive(:source)
parse_source_from_properties
end
@@ -86,35 +85,4 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category:
parse_source_from_properties
end
end
-
- context 'when container_scanning properties are present' do
- let(:properties) do
- [
- { 'name' => 'gitlab:meta:schema_version', 'value' => '1' },
- { 'name' => 'gitlab:container_scanning:image:name', 'value' => 'photon' },
- { 'name' => 'gitlab:container_scanning:image:tag', 'value' => '5.0-20231007' },
- { 'name' => 'gitlab:container_scanning:operating_system:name', 'value' => 'Photon OS' },
- { 'name' => 'gitlab:container_scanning:operating_system:version', 'value' => '5.0' }
- ]
- end
-
- let(:expected_input) do
- {
- 'image' => {
- 'name' => 'photon',
- 'tag' => '5.0-20231007'
- },
- 'operating_system' => {
- 'name' => 'Photon OS',
- 'version' => '5.0'
- }
- }
- end
-
- it 'passes only supported properties to the container scanning parser' do
- expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).to receive(:source).with(expected_input)
-
- parse_source_from_properties
- end
- end
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
deleted file mode 100644
index 1b4426c4e5a..00000000000
--- a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning, feature_category: :container_scanning do
- subject { described_class.source(property_data) }
-
- context 'when all property data is present' do
- let(:property_data) do
- {
- 'image' => {
- 'name' => 'photon',
- 'tag' => '5.0-20231007'
- },
- 'operating_system' => {
- 'name' => 'Photon OS',
- 'version' => '5.0'
- }
- }
- end
-
- it 'returns expected source data' do
- is_expected.to have_attributes(
- source_type: :container_scanning,
- data: property_data
- )
- end
- end
-
- context 'when required properties are missing' do
- let(:property_data) do
- {
- 'image' => {
- 'tag' => '5.0-20231007'
- },
- 'operating_system' => {
- 'name' => 'Photon OS',
- 'version' => '5.0'
- }
- }
- end
-
- it { is_expected.to be_nil }
- end
-end
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 28fbd4d883f..6b4984ceaf2 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -47,10 +47,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
.with(object)
.and_return(false)
- expect(Gitlab::Import::Logger)
+ expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
- import_type: :github,
project_id: 1,
importer: 'MyImporter',
message: '1 object_types fetched'
@@ -82,10 +81,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
.with(object)
.and_return(true)
- expect(Gitlab::Import::Logger)
+ expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
- import_type: :github,
project_id: 1,
importer: 'MyImporter',
message: '0 object_types fetched'
@@ -145,14 +143,13 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
}
)
- expect(Gitlab::Import::Logger)
+ expect(Gitlab::GithubImport::Logger)
.to receive(:error)
.with(
- import_type: :github,
project_id: 1,
importer: 'MyImporter',
message: ['Title is invalid'],
- github_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type }
+ external_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type }
)
expect(Gitlab::GithubImport::ObjectCounter)
@@ -172,7 +169,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
expect(errors).not_to be_empty
expect(errors[0][:validation_errors].full_messages).to match_array(['Title is invalid'])
- expect(errors[0][:github_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type })
+ expect(errors[0][:external_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type })
end
end
end
@@ -182,11 +179,10 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
it 'bulk inserts rows into the database' do
rows = [{ title: 'Foo' }] * 10
- expect(Gitlab::Import::Logger)
+ expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.twice
.with(
- import_type: :github,
project_id: 1,
importer: 'MyImporter',
message: '5 object_types imported'
@@ -243,7 +239,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers
importer.bulk_insert_failures([{
validation_errors: error,
- github_identifiers: { id: 123456 }
+ external_identifiers: { id: 123456 }
}])
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 4b0d61e3188..5f321a15de9 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -316,7 +316,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
allow_retry
expect(client).to receive(:requests_remaining?).twice.and_return(true)
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
+ expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once
expect(client.with_rate_limit(&block_to_rate_limit)).to eq({})
end
@@ -337,7 +337,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
it 'retries on error and succeeds' do
allow_retry
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
+ expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once
expect(client.with_rate_limit(&block_to_rate_limit)).to eq({})
end
@@ -723,7 +723,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
it 'retries on error and succeeds' do
allow_retry(:post)
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
+ expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once
expect(client.search_repos_by_name_graphql('test')).to eq({})
end
diff --git a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb
index 7890561bf2d..b44f1ec85f3 100644
--- a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project issue attachments' do
+ expect(project.issues).to receive(:id_not_in).with([]).and_return(project.issues)
expect(project.issues).to receive(:select).with(:id, :description, :iid).and_call_original
expect_next_instances_of(
@@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do
it "doesn't import this issue attachments" do
importer.mark_as_imported(issue_1)
+ expect(project.issues).to receive(:id_not_in).with([issue_1.id.to_s]).and_call_original
expect_next_instance_of(
Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs
) do |note_attachments_importer|
diff --git a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb
index e5aa17dd81e..381cb17bb52 100644
--- a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter do
+RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project merge request attachments' do
+ expect(project.merge_requests).to receive(:id_not_in).with([]).and_return(project.merge_requests)
expect(project.merge_requests).to receive(:select).with(:id, :description, :iid).and_call_original
expect_next_instances_of(
@@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte
it "doesn't import this merge request attachments" do
importer.mark_as_imported(merge_request_1)
+ expect(project.merge_requests).to receive(:id_not_in).with([merge_request_1.id.to_s]).and_call_original
expect_next_instance_of(
Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs
) do |note_attachments_importer|
diff --git a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb
index 7ed353e1b71..5b3ad032702 100644
--- a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
@@ -18,6 +18,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project user note' do
+ expect(project.notes).to receive(:id_not_in).with([]).and_call_original
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).twice.and_return(importer_stub)
expect(importer_stub).to receive(:execute).twice
@@ -29,6 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do
it "doesn't import this note" do
importer.mark_as_imported(note_1)
+ expect(project.notes).to receive(:id_not_in).with([note_1.id.to_s]).and_call_original
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).once.and_return(importer_stub)
expect(importer_stub).to receive(:execute).once
diff --git a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb
index e1b009c3eeb..c1c19c40afb 100644
--- a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project) }
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do
let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] }
it 'imports each project release' do
+ expect(project.releases).to receive(:id_not_in).with([]).and_return(project.releases)
expect(project.releases).to receive(:select).with(:id, :description, :tag).and_call_original
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
@@ -30,6 +31,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do
it "doesn't import this release" do
importer.mark_as_imported(release_1)
+ expect(project.releases).to receive(:id_not_in).with([release_1.id.to_s]).and_call_original
expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new)
.with(*importer_attrs).once.and_return(importer_stub)
expect(importer_stub).to receive(:execute).once
diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
index fc8d9cee066..0328a36b646 100644
--- a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
@@ -50,13 +50,12 @@ feature_category: :importers do
label = { id: 1, name: 'bug,bug', color: 'ffffff' }
expect(importer).to receive(:each_label).and_return([label])
- expect(Gitlab::Import::Logger).to receive(:error)
+ expect(Gitlab::GithubImport::Logger).to receive(:error)
.with(
- import_type: :github,
project_id: project.id,
importer: described_class.name,
message: ['Title is invalid'],
- github_identifiers: { title: 'bug,bug', object_type: :label }
+ external_identifiers: { title: 'bug,bug', object_type: :label }
)
rows, errors = importer.build_labels
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index cf44d510c80..fa7283d210b 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -80,13 +80,12 @@ RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab
.to receive(:each_milestone)
.and_return([milestone])
- expect(Gitlab::Import::Logger).to receive(:error)
+ expect(Gitlab::GithubImport::Logger).to receive(:error)
.with(
- import_type: :github,
project_id: project.id,
importer: described_class.name,
message: ["Title can't be blank"],
- github_identifiers: { iid: 2, object_type: :milestone, title: nil }
+ external_identifiers: { iid: 2, object_type: :milestone, title: nil }
)
rows, errors = importer.build_milestones
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index eddde272d2c..cfd75fba849 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_cat
it 'updates the repository' do
importer = described_class.new(project, client)
- expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect_next_instance_of(Gitlab::GithubImport::Logger) do |logger|
expect(logger)
.to receive(:info)
.with(an_instance_of(Hash))
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
index a3d20af22c7..1cfbe8e20ae 100644
--- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter, feature_categor
expect(errors[0][:validation_errors].full_messages).to match_array(
['Description is too long (maximum is 1000000 characters)']
)
- expect(errors[0][:github_identifiers]).to eq({ tag: '1.0', object_type: :release })
+ expect(errors[0][:external_identifiers]).to eq({ tag: '1.0', object_type: :release })
end
end
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
index f12cbe4f82f..fd9d609992d 100644
--- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -131,19 +131,19 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import
end
it 'generates the correct hash for a relation with included attributes' do
- setup_yaml(tree: { project: [:issues] },
- included_attributes: { issues: [:name, :description] })
+ setup_yaml(
+ tree: { project: [:issues] },
+ included_attributes: { issues: [:name, :description] }
+ )
is_expected.to match(
- include: [{ issues: { include: [],
- only: [:name, :description] } }],
+ include: [{ issues: { include: [], only: [:name, :description] } }],
preload: { issues: nil }
)
end
it 'generates the correct hash for a relation with excluded attributes' do
- setup_yaml(tree: { project: [:issues] },
- excluded_attributes: { issues: [:name] })
+ setup_yaml(tree: { project: [:issues] }, excluded_attributes: { issues: [:name] })
is_expected.to match(
include: [{ issues: { except: [:name],
@@ -153,25 +153,23 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import
end
it 'generates the correct hash for a relation with both excluded and included attributes' do
- setup_yaml(tree: { project: [:issues] },
- excluded_attributes: { issues: [:name] },
- included_attributes: { issues: [:description] })
+ setup_yaml(
+ tree: { project: [:issues] },
+ excluded_attributes: { issues: [:name] },
+ included_attributes: { issues: [:description] }
+ )
is_expected.to match(
- include: [{ issues: { except: [:name],
- include: [],
- only: [:description] } }],
+ include: [{ issues: { except: [:name], include: [], only: [:description] } }],
preload: { issues: nil }
)
end
it 'generates the correct hash for a relation with custom methods' do
- setup_yaml(tree: { project: [:issues] },
- methods: { issues: [:name] })
+ setup_yaml(tree: { project: [:issues] }, methods: { issues: [:name] })
is_expected.to match(
- include: [{ issues: { include: [],
- methods: [:name] } }],
+ include: [{ issues: { include: [], methods: [:name] } }],
preload: { issues: nil }
)
end
diff --git a/spec/lib/gitlab/import_export/base/object_builder_spec.rb b/spec/lib/gitlab/import_export/base/object_builder_spec.rb
index 38c3b23db36..3c69a6a7746 100644
--- a/spec/lib/gitlab/import_export/base/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/base/object_builder_spec.rb
@@ -4,11 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Base::ObjectBuilder do
let(:project) do
- create(:project, :repository,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project')
+ create(
+ :project, :repository,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project'
+ )
end
let(:klass) { Milestone }
diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
index 4ef8f4b5d76..5e63804c51c 100644
--- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
@@ -11,14 +11,16 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
let(:excluded_keys) { [] }
subject do
- described_class.create(relation_sym: relation_sym, # rubocop:disable Rails/SaveBang
- relation_hash: relation_hash,
- relation_index: 1,
- object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
- members_mapper: members_mapper,
- user: user,
- importable: project,
- excluded_keys: excluded_keys)
+ described_class.create( # rubocop:disable Rails/SaveBang
+ relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ relation_index: 1,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
+ members_mapper: members_mapper,
+ user: user,
+ importable: project,
+ excluded_keys: excluded_keys
+ )
end
describe '#create' do
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
index 5ef9eb78d3b..144617055ab 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -12,9 +12,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) }
let(:restorer) do
- described_class.new(path_to_bundle: bundle_path,
- shared: shared,
- importable: project)
+ described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project)
end
before do
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index 02419267f0e..dfc7202194d 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -217,17 +217,18 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_
release = create(:release)
group = create(:group)
- project = create(:project,
- :public,
- :repository,
- :issues_disabled,
- :wiki_enabled,
- :builds_private,
- description: 'description',
- releases: [release],
- group: group,
- approvals_before_merge: 1
- )
+ project = create(
+ :project,
+ :public,
+ :repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
+ description: 'description',
+ releases: [release],
+ group: group,
+ approvals_before_merge: 1
+ )
issue = create(:issue, assignees: [user], project: project)
snippet = create(:project_snippet, project: project)
@@ -249,10 +250,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_
create(:discussion_note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
create(:note, noteable: snippet, project: project)
- create(:note_on_commit,
- author: user,
- project: project,
- commit_id: ci_build.pipeline.sha)
+ create(:note_on_commit, author: user, project: project, commit_id: ci_build.pipeline.sha)
create(:resource_label_event, label: project_label, issue: issue)
create(:resource_label_event, label: group_label, merge_request: merge_request)
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 3ca9f727033..17d416b0f0a 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -16,10 +16,12 @@ RSpec.describe Gitlab::ImportExport::MergeRequestParser do
let(:diff_head_sha) { SecureRandom.hex(20) }
let(:parsed_merge_request) do
- described_class.new(project,
- diff_head_sha,
- merge_request,
- merge_request.as_json).parse!
+ described_class.new(
+ project,
+ diff_head_sha,
+ merge_request,
+ merge_request.as_json
+ ).parse!
end
after do
diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
index 43794ce01a3..20e176bf6fd 100644
--- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
@@ -6,12 +6,15 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
let!(:group) { create(:group, :private) }
let!(:subgroup) { create(:group, :private, parent: group) }
let!(:project) do
- create(:project, :repository,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project',
- group: subgroup)
+ create(
+ :project,
+ :repository,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: subgroup
+ )
end
let(:lru_cache) { subject.send(:lru_cache) }
@@ -19,10 +22,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
context 'request store is not active' do
subject do
- described_class.new(Label,
- 'title' => 'group label',
- 'project' => project,
- 'group' => project.group)
+ described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group)
end
it 'ignore cache initialize' do
@@ -33,10 +33,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
context 'request store is active', :request_store do
subject do
- described_class.new(Label,
- 'title' => 'group label',
- 'project' => project,
- 'group' => project.group)
+ described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group)
end
it 'initialize cache in memory' do
@@ -71,27 +68,33 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
it 'finds the existing group label' do
group_label = create(:group_label, name: 'group label', group: project.group)
- expect(described_class.build(Label,
- 'title' => 'group label',
- 'project' => project,
- 'group' => project.group)).to eq(group_label)
+ expect(described_class.build(
+ Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group
+ )).to eq(group_label)
end
it 'finds the existing group label in root ancestor' do
group_label = create(:group_label, name: 'group label', group: group)
- expect(described_class.build(Label,
- 'title' => 'group label',
- 'project' => project,
- 'group' => group)).to eq(group_label)
+ expect(described_class.build(
+ Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => group
+ )).to eq(group_label)
end
it 'creates a new project label' do
- label = described_class.build(Label,
- 'title' => 'group label',
- 'project' => project,
- 'group' => project.group,
- 'group_id' => project.group.id)
+ label = described_class.build(
+ Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group,
+ 'group_id' => project.group.id
+ )
expect(label.persisted?).to be true
expect(label).to be_an_instance_of(ProjectLabel)
@@ -103,26 +106,32 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
it 'finds the existing group milestone' do
milestone = create(:milestone, name: 'group milestone', group: project.group)
- expect(described_class.build(Milestone,
- 'title' => 'group milestone',
- 'project' => project,
- 'group' => project.group)).to eq(milestone)
+ expect(described_class.build(
+ Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group
+ )).to eq(milestone)
end
it 'finds the existing group milestone in root ancestor' do
milestone = create(:milestone, name: 'group milestone', group: group)
- expect(described_class.build(Milestone,
- 'title' => 'group milestone',
- 'project' => project,
- 'group' => group)).to eq(milestone)
+ expect(described_class.build(
+ Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => group
+ )).to eq(milestone)
end
it 'creates a new milestone' do
- milestone = described_class.build(Milestone,
- 'title' => 'group milestone',
- 'project' => project,
- 'group' => project.group)
+ milestone = described_class.build(
+ Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group
+ )
expect(milestone.persisted?).to be true
end
@@ -132,12 +141,14 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
clashing_iid = 1
create(:milestone, iid: clashing_iid, project: project)
- milestone = described_class.build(Milestone,
- 'iid' => clashing_iid,
- 'title' => 'milestone',
- 'project' => project,
- 'group' => nil,
- 'group_id' => nil)
+ milestone = described_class.build(
+ Milestone,
+ 'iid' => clashing_iid,
+ 'title' => 'milestone',
+ 'project' => project,
+ 'group' => nil,
+ 'group_id' => nil
+ )
expect(milestone.persisted?).to be true
expect(Milestone.count).to eq(2)
@@ -173,34 +184,45 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
context 'merge_request' do
it 'finds the existing merge_request' do
- merge_request = create(:merge_request, title: 'MergeRequest', iid: 7, target_project: project, source_project: project)
- expect(described_class.build(MergeRequest,
- 'title' => 'MergeRequest',
- 'source_project_id' => project.id,
- 'target_project_id' => project.id,
- 'source_branch' => 'SourceBranch',
- 'iid' => 7,
- 'target_branch' => 'TargetBranch',
- 'author_id' => project.creator.id)).to eq(merge_request)
+ merge_request = create(
+ :merge_request,
+ title: 'MergeRequest',
+ iid: 7,
+ target_project: project,
+ source_project: project
+ )
+
+ expect(described_class.build(
+ MergeRequest,
+ 'title' => 'MergeRequest',
+ 'source_project_id' => project.id,
+ 'target_project_id' => project.id,
+ 'source_branch' => 'SourceBranch',
+ 'iid' => 7,
+ 'target_branch' => 'TargetBranch',
+ 'author_id' => project.creator.id
+ )).to eq(merge_request)
end
it 'creates a new merge_request' do
- merge_request = described_class.build(MergeRequest,
- 'title' => 'MergeRequest',
- 'iid' => 8,
- 'source_project_id' => project.id,
- 'target_project_id' => project.id,
- 'source_branch' => 'SourceBranch',
- 'target_branch' => 'TargetBranch',
- 'author_id' => project.creator.id)
+ merge_request = described_class.build(
+ MergeRequest,
+ 'title' => 'MergeRequest',
+ 'iid' => 8,
+ 'source_project_id' => project.id,
+ 'target_project_id' => project.id,
+ 'source_branch' => 'SourceBranch',
+ 'target_branch' => 'TargetBranch',
+ 'author_id' => project.creator.id
+ )
+
expect(merge_request.persisted?).to be true
end
end
context 'merge request diff commit users' do
it 'finds the existing user' do
- user = MergeRequest::DiffCommitUser
- .find_or_create('Alice', 'alice@example.com')
+ user = MergeRequest::DiffCommitUser.find_or_create('Alice', 'alice@example.com')
found = described_class.build(
MergeRequest::DiffCommitUser,
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index c214f36046c..14af3028a6e 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -854,12 +854,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
end
let!(:project) do
- create(:project,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project',
- group: group)
+ create(
+ :project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: group
+ )
end
before do
@@ -890,12 +892,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'with existing group models' do
let(:group) { create(:group).tap { |g| g.add_maintainer(user) } }
let!(:project) do
- create(:project,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project',
- group: group)
+ create(
+ :project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: group
+ )
end
before do
@@ -926,12 +930,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'with clashing milestones on IID' do
let(:group) { create(:group).tap { |g| g.add_maintainer(user) } }
let!(:project) do
- create(:project,
- :builds_disabled,
- :issues_disabled,
- name: 'project',
- path: 'project',
- group: group)
+ create(
+ :project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: group
+ )
end
before do
@@ -1143,8 +1149,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') }
let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') }
let_it_be(:project) do
- create(:project, :builds_disabled, :issues_disabled,
- { name: 'project', path: 'project' })
+ create(:project, :builds_disabled, :issues_disabled, { name: 'project', path: 'project' })
end
let(:shared) { project.import_export_shared }
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
index 408ed3a2176..37a59a68188 100644
--- a/spec/lib/gitlab/import_export/shared_spec.rb
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -74,12 +74,12 @@ RSpec.describe Gitlab::ImportExport::Shared do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.with(error, hash_including(
- importer: 'Import/Export',
- project_id: project.id,
- project_name: project.name,
- project_path: project.full_path,
- import_jid: import_state.jid
- ))
+ importer: 'Import/Export',
+ project_id: project.id,
+ project_name: project.name,
+ project_path: project.full_path,
+ import_jid: import_state.jid
+ ))
subject.error(error)
end
diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
index 2f39cb560d0..d7b1b180e2e 100644
--- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb
@@ -10,10 +10,12 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
let(:shared) { project.import_export_shared }
let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) }
let(:restorer) do
- described_class.new(user: user,
- shared: shared,
- snippet: snippet,
- path_to_bundle: snippet_bundle_path)
+ described_class.new(
+ user: user,
+ shared: shared,
+ snippet: snippet,
+ path_to_bundle: snippet_bundle_path
+ )
end
after do
diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
index e348e8f7991..4a9a01475cb 100644
--- a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer, :clean_gitlab_redis_r
let(:bundle_dir) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) }
let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) }
let(:restorer) do
- described_class.new(user: user,
- shared: shared,
- project: project)
+ described_class.new(user: user, shared: shared, project: project)
end
after do
diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb
new file mode 100644
index 00000000000..d7e0ee2079c
--- /dev/null
+++ b/spec/models/ci/catalog/components_project_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_composition do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:files) do
+ {
+ 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1",
+ 'templates/dast/template.yml' => 'image: alpine_2',
+ 'templates/template.yml' => 'image: alpine_3',
+ 'templates/blank-yaml.yml' => '',
+ 'templates/dast/sub-folder/template.yml' => 'image: alpine_4',
+ 'tests/test.yml' => 'image: alpine_5',
+ 'README.md' => 'Read me'
+ }
+ end
+
+ let_it_be(:project) do
+ create(
+ :project, :custom_repo,
+ description: 'Simple, complex, and other components',
+ files: files
+ )
+ end
+
+ let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
+
+ let(:components_project) { described_class.new(project, project.default_branch) }
+
+ describe '#fetch_component_paths' do
+ it 'retrieves all the paths for valid components' do
+ paths = components_project.fetch_component_paths(project.default_branch)
+
+ expect(paths).to contain_exactly(
+ 'templates/blank-yaml.yml', 'templates/dast/template.yml', 'templates/secret-detection.yml',
+ 'templates/template.yml'
+ )
+ end
+ end
+
+ describe '#extract_component_name' do
+ context 'with invalid component path' do
+ it 'raises an error' do
+ expect(components_project.extract_component_name('not-template/this-is-wrong.yml')).to be_nil
+ end
+ end
+
+ context 'with valid component paths' do
+ where(:path, :name) do
+ 'templates/secret-detection.yml' | 'secret-detection'
+ 'templates/dast/template.yml' | 'dast'
+ 'templates/template.yml' | 'template'
+ 'templates/blank-yaml.yml' | 'blank-yaml'
+ end
+
+ with_them do
+ it 'extracts the component name from the path' do
+ expect(components_project.extract_component_name(path)).to eq(name)
+ end
+ end
+ end
+ end
+
+ describe '#extract_inputs' do
+ context 'with valid inputs' do
+ it 'extracts the inputs from a blob' do
+ blob = "spec:\n inputs:\n website:\n---\nimage: alpine_1"
+
+ expect(components_project.extract_inputs(blob)).to eq({ website: nil })
+ end
+ end
+
+ context 'with invalid inputs' do
+ it 'raises InvalidFormatError' do
+ blob = "spec:\n inputs:\n website:\n---\nsome: invalid: string"
+
+ expect do
+ components_project.extract_inputs(blob)
+ end.to raise_error(::Gitlab::Config::Loader::FormatError,
+ /mapping values are not allowed in this context/)
+ end
+ end
+ end
+
+ describe '#fetch_component' do
+ where(:component_name, :content, :path) do
+ 'secret-detection' | "spec:\n inputs:\n website:\n---\nimage: alpine_1" | 'templates/secret-detection.yml'
+ 'dast' | 'image: alpine_2' | 'templates/dast/template.yml'
+ 'template' | 'image: alpine_3' | 'templates/template.yml'
+ 'blank-yaml' | '' | 'templates/blank-yaml.yml'
+ end
+
+ with_them do
+ it 'fetches the content for a component' do
+ data = components_project.fetch_component(component_name)
+
+ expect(data.path).to eq(path)
+ expect(data.content).to eq(content)
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/pushover_spec.rb b/spec/models/integrations/pushover_spec.rb
index 8286fd20669..c576340a78a 100644
--- a/spec/models/integrations/pushover_spec.rb
+++ b/spec/models/integrations/pushover_spec.rb
@@ -62,4 +62,12 @@ RSpec.describe Integrations::Pushover do
expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once
end
end
+
+ describe '#avatar_url' do
+ it 'returns the avatar image path' do
+ expect(subject.avatar_url).to eq(
+ ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg')
+ )
+ end
+ end
end
diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb
index 8cb4043df65..e22989f3ce2 100644
--- a/spec/models/ml/model_spec.rb
+++ b/spec/models/ml/model_spec.rb
@@ -119,6 +119,26 @@ RSpec.describe Ml::Model, feature_category: :mlops do
end
end
+ describe 'with_version_count' do
+ let(:model) { existing_model }
+
+ subject { described_class.with_version_count.find_by(id: model.id).version_count }
+
+ context 'when model has versions' do
+ before do
+ create(:ml_model_versions, model: model)
+ end
+
+ it { is_expected.to eq(1) }
+ end
+
+ context 'when model has no versions' do
+ let(:model) { another_existing_model }
+
+ it { is_expected.to eq(0) }
+ end
+ end
+
describe '#by_project_and_id' do
let(:id) { existing_model.id }
let(:project_id) { existing_model.project.id }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 570a243154d..1a0fc34a1cd 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6801,6 +6801,17 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ describe '.with_package_registry_enabled' do
+ subject { described_class.with_package_registry_enabled }
+
+ it 'returns projects with the package registry enabled' do
+ project_1 = create(:project)
+ create(:project, package_registry_access_level: ProjectFeature::DISABLED, packages_enabled: false)
+
+ expect(subject).to contain_exactly(project_1)
+ end
+ end
+
describe '.deployments' do
subject { project.deployments }
diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
index d9ea7bc7f82..11f6d816fc1 100644
--- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
@@ -72,6 +72,31 @@ RSpec.shared_context 'ProjectPolicyTable context' do
:private | :disabled | :anonymous | nil | 0
end
+ # group_level, :membership, :admin_mode, :expected_count
+ # We need a new table because epics are at a group level only.
+ def permission_table_for_epics_access
+ :public | :admin | true | 1
+ :public | :admin | false | 1
+ :public | :reporter | nil | 1
+ :public | :guest | nil | 1
+ :public | :non_member | nil | 1
+ :public | :anonymous | nil | 1
+
+ :internal | :admin | true | 1
+ :internal | :admin | false | 0
+ :internal | :reporter | nil | 0
+ :internal | :guest | nil | 0
+ :internal | :non_member | nil | 0
+ :internal | :anonymous | nil | 0
+
+ :private | :admin | true | 1
+ :private | :admin | false | 0
+ :private | :reporter | nil | 0
+ :private | :guest | nil | 0
+ :private | :non_member | nil | 0
+ :private | :anonymous | nil | 0
+ end
+
# project_level, :feature_access_level, :membership, :admin_mode, :expected_count
def permission_table_for_guest_feature_access
:public | :enabled | :admin | true | 1
diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb
index 8f7f305b186..1d2b93b9287 100644
--- a/spec/workers/auto_devops/disable_worker_spec.rb
+++ b/spec/workers/auto_devops/disable_worker_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe AutoDevops::DisableWorker, '#perform', feature_category: :auto_devops do
diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
index dc715c3026b..d11b044b093 100644
--- a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
+++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i
let(:log_attributes) do
{
'user_id' => user.id,
- 'github_identifiers' => { 'id': gist_object.id },
+ 'external_identifiers' => { 'id': gist_object.id },
'class' => 'Gitlab::GithubGistsImport::ImportGistWorker',
'correlation_id' => 'new-correlation-id',
'jid' => nil,
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i
it 'raises an error' do
expect(Gitlab::GithubImport::Logger)
.to receive(:error)
- .with(log_attributes.merge('message' => 'importer failed', 'error.message' => '_some_error_'))
+ .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => '_some_error_'))
expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect { subject.perform(user.id, gist_hash, 'some_key') }.to raise_error(StandardError)
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i
it 'tracks and logs error' do
expect(Gitlab::GithubImport::Logger)
.to receive(:error)
- .with(log_attributes.merge('message' => 'importer failed', 'error.message' => 'error_message'))
+ .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => 'error_message'))
expect(Gitlab::JobWaiter)
.to receive(:notify)
.with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
diff --git a/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb
index 220f2bb0c75..0bd371b6c97 100644
--- a/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb
+++ b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::GithubGistsImport::StartImportWorker, feature_category: :
expect(Gitlab::GithubImport::Logger)
.to receive(:error)
- .with(log_attributes.merge('message' => 'import failed', 'error.message' => exception.message))
+ .with(log_attributes.merge('message' => 'import failed', 'exception.message' => exception.message))
expect { worker.perform(user.id, token) }.to raise_error(StandardError)
end
diff --git a/spec/workers/integrations/execute_worker_spec.rb b/spec/workers/integrations/execute_worker_spec.rb
index 369fc5fd091..10e290005cc 100644
--- a/spec/workers/integrations/execute_worker_spec.rb
+++ b/spec/workers/integrations/execute_worker_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Integrations::ExecuteWorker, '#perform', feature_category: :integrations do
diff --git a/spec/workers/partition_creation_worker_spec.rb b/spec/workers/partition_creation_worker_spec.rb
index ab525fd5ce2..625e86ad852 100644
--- a/spec/workers/partition_creation_worker_spec.rb
+++ b/spec/workers/partition_creation_worker_spec.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-#
+
require 'spec_helper'
RSpec.describe PartitionCreationWorker, feature_category: :database do
diff --git a/spec/workers/projects/after_import_worker_spec.rb b/spec/workers/projects/after_import_worker_spec.rb
index 5af4f49d6e0..18105488549 100644
--- a/spec/workers/projects/after_import_worker_spec.rb
+++ b/spec/workers/projects/after_import_worker_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Projects::AfterImportWorker, feature_category: :importers do
message: 'Project housekeeping failed',
project_full_path: project.full_path,
project_id: project.id,
- 'error.message' => exception.to_s
+ 'exception.message' => exception.to_s
}).and_call_original
subject
diff --git a/spec/workers/projects/delete_branch_worker_spec.rb b/spec/workers/projects/delete_branch_worker_spec.rb
index 771ab3def84..ddd65e51383 100644
--- a/spec/workers/projects/delete_branch_worker_spec.rb
+++ b/spec/workers/projects/delete_branch_worker_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
# rubocop: disable Gitlab/ServiceResponse
require 'spec_helper'
diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb
index cd58dd93b80..1e82b0f2845 100644
--- a/spec/workers/web_hook_worker_spec.rb
+++ b/spec/workers/web_hook_worker_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe WebHookWorker, feature_category: :integrations do