Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-22 12:07:28 +00:00
parent 4c7e34071e
commit 4220cf46a3
69 changed files with 889 additions and 125 deletions

View File

@ -11,7 +11,6 @@ Style/OpenStructUse:
- lib/gitlab/git/diff_collection.rb
- lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
- lib/gitlab/testing/request_inspector_middleware.rb
- lib/mattermost/session.rb
- spec/factories/go_module_versions.rb
- spec/factories/wiki_pages.rb
- spec/graphql/mutations/branches/create_spec.rb

View File

@ -1 +1 @@
f21e9469e94600f50ecb01b98d46f54dd7b33b5c
719c5a5bd2b5ddb54de519d6873ccb1636f7b450

View File

@ -1,6 +1,6 @@
<script>
import { uniqueId } from 'lodash';
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
@ -8,11 +8,12 @@ export default {
components: {
GlButton,
GlModal,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
inject: ['path'],
inject: ['path', 'name'],
data() {
return {
modalId: uniqueId('remove-topic-avatar-'),
@ -25,8 +26,8 @@ export default {
},
i18n: {
remove: __('Remove avatar'),
title: __('Confirm remove avatar'),
body: __('Avatar will be removed. Are you sure?'),
title: __('Remove topic avatar'),
body: __('Topic avatar for %{name} will be removed. This cannot be undone.'),
},
modal: {
actionPrimary: {
@ -57,7 +58,9 @@ export default {
:modal-id="modalId"
size="sm"
@primary="deleteApplication"
>{{ $options.i18n.body }}
><gl-sprintf :message="$options.i18n.body"
><template #name>{{ name }}</template></gl-sprintf
>
<form ref="deleteForm" method="post" :action="path">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />

View File

@ -8,12 +8,13 @@ export default () => {
return false;
}
const { path } = el.dataset;
const { path, name } = el.dataset;
return new Vue({
el,
provide: {
path,
name,
},
render(h) {
return h(RemoveAvatar);

View File

@ -12,6 +12,7 @@ import ZenMode from '~/zen_mode';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
const DATA_ISSUES_NEW_PATH = 'data-new-issue-path';
function organizeQuery(obj, isFallbackKey = false) {
if (!obj[MR_SOURCE_BRANCH] && !obj[MR_TARGET_BRANCH]) {
@ -68,6 +69,7 @@ export default class IssuableForm {
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
this.zenMode = new ZenMode();
this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH);
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
@ -104,8 +106,8 @@ export default class IssuableForm {
}
initAutosave() {
const { search } = document.location;
const searchTerm = format(search);
const { search, pathname } = document.location;
const searchTerm = this.newIssuePath === pathname ? '' : format(search);
const fallbackKey = getFallbackKey();
this.autosave = new Autosave(

View File

@ -48,4 +48,4 @@ export function initJiraConnect() {
});
}
document.addEventListener('DOMContentLoaded', initJiraConnect);
initJiraConnect();

View File

@ -1,8 +1,6 @@
import initClustersListApp from '~/clusters_list';
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout);
initClustersListApp();
});
const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout);
initClustersListApp();

View File

@ -10,21 +10,19 @@ import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
import initConfirmDanger from '~/init_confirm_danger';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
initConfirmDanger();
initSettingsPanels();
initTransferGroupForm();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
);
mountBadgeSettings(GROUP_BADGE);
initFilePickers();
initConfirmDanger();
initSettingsPanels();
initTransferGroupForm();
dirtySubmitFactory(
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
);
mountBadgeSettings(GROUP_BADGE);
// Initialize Subgroups selector
groupsSelect();
// Initialize Subgroups selector
groupsSelect();
projectSelect();
projectSelect();
initSearchSettings();
initCascadingSettingsLockPopovers();
});
initSearchSettings();
initCascadingSettingsLockPopovers();

View File

@ -1,3 +1,3 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
document.addEventListener('DOMContentLoaded', initProfilePreferences);
initProfilePreferences();

View File

@ -10,36 +10,34 @@ import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_to
import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
// Initialize expandable settings panels
initSettingsPanels();
const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) {
const runnerTokenSecretValue = new SecretValues({
container: runnerToken,
});
runnerTokenSecretValue.init();
}
initVariableList();
// hide extra auto devops settings based checkbox state
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
const { target } = event;
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
const runnerToken = document.querySelector('.js-secret-runner-token');
if (runnerToken) {
const runnerTokenSecretValue = new SecretValues({
container: runnerToken,
});
runnerTokenSecretValue.init();
}
registrySettingsApp();
initDeployFreeze();
initVariableList();
initSettingsPipelinesTriggers();
initArtifactsSettings();
initSharedRunnersToggle();
initInstallRunner();
initRunnerAwsDeployments();
initTokenAccess();
// hide extra auto devops settings based checkbox state
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
const { target } = event;
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
registrySettingsApp();
initDeployFreeze();
initSettingsPipelinesTriggers();
initArtifactsSettings();
initSharedRunnersToggle();
initInstallRunner();
initRunnerAwsDeployments();
initTokenAccess();

View File

@ -1,9 +1,7 @@
import MirrorRepos from '~/mirrors/mirror_repos';
import initForm from '../form';
document.addEventListener('DOMContentLoaded', () => {
initForm();
initForm();
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
});
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();

View File

@ -239,6 +239,12 @@ module IssuesHelper
)
end
def issues_form_data(project)
{
new_issue_path: new_project_issue_path(project)
}
end
# Overridden in EE
def scoped_labels_available?(parent)
false

View File

@ -23,7 +23,7 @@ class CustomerRelations::Contact < ApplicationRecord
validates :last_name, presence: true, length: { maximum: 255 }
validates :email, length: { maximum: 255 }
validates :description, length: { maximum: 1024 }
validates :email, uniqueness: { scope: :group_id }
validates :email, uniqueness: { case_sensitive: false, scope: :group_id }
validate :validate_email_format
validate :validate_root_group
@ -42,7 +42,7 @@ class CustomerRelations::Contact < ApplicationRecord
def self.find_ids_by_emails(group, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
where(group: group, email: emails).pluck(:id)
where(group: group).where('lower(email) in (?)', emails.map(&:downcase)).pluck(:id)
end
def self.exists_for_group?(group)
@ -51,6 +51,34 @@ class CustomerRelations::Contact < ApplicationRecord
exists?(group: group)
end
def self.move_to_root_group(group)
update_query = <<~SQL
UPDATE #{CustomerRelations::IssueContact.table_name}
SET contact_id = new_contacts.id
FROM #{table_name} AS existing_contacts
JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
SQL
connection.execute(sanitize_sql([
update_query,
old_group_id: group.root_ancestor.id,
new_group_id: group.id
]))
dupes_query = <<~SQL
DELETE FROM #{table_name} AS existing_contacts
USING #{table_name} AS new_contacts
WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
SQL
connection.execute(sanitize_sql([
dupes_query,
old_group_id: group.root_ancestor.id,
new_group_id: group.id
]))
where(group: group).update_all(group_id: group.root_ancestor.id)
end
private
def validate_email_format

View File

@ -8,6 +8,8 @@ class CustomerRelations::IssueContact < ApplicationRecord
validate :contact_belongs_to_root_group
BATCH_DELETE_SIZE = 1_000
def self.find_contact_ids_by_emails(issue_id, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
@ -17,9 +19,17 @@ class CustomerRelations::IssueContact < ApplicationRecord
end
def self.delete_for_project(project_id)
joins(:issue)
.where(issues: { project_id: project_id })
.delete_all
loop do
deleted_records = joins(:issue).where(issues: { project_id: project_id }).limit(BATCH_DELETE_SIZE).delete_all
break if deleted_records == 0
end
end
def self.delete_for_group(group)
loop do
deleted_records = joins(issue: :project).where(projects: { namespace: group.self_and_descendants }).limit(BATCH_DELETE_SIZE).delete_all
break if deleted_records == 0
end
end
private

View File

@ -26,6 +26,34 @@ class CustomerRelations::Organization < ApplicationRecord
.where('LOWER(name) = LOWER(?)', name)
end
def self.move_to_root_group(group)
update_query = <<~SQL
UPDATE #{CustomerRelations::Contact.table_name}
SET organization_id = new_organizations.id
FROM #{table_name} AS existing_organizations
JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
SQL
connection.execute(sanitize_sql([
update_query,
old_group_id: group.root_ancestor.id,
new_group_id: group.id
]))
dupes_query = <<~SQL
DELETE FROM #{table_name} AS existing_organizations
USING #{table_name} AS new_organizations
WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
SQL
connection.execute(sanitize_sql([
dupes_query,
old_group_id: group.root_ancestor.id,
new_group_id: group.id
]))
where(group: group).update_all(group_id: group.root_ancestor.id)
end
private
def validate_root_group

View File

@ -61,8 +61,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
# AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'

View File

@ -15,6 +15,7 @@ class Namespace < ApplicationRecord
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
include EachBatch
include BlocksUnsafeSerialization
# Temporary column used for back-filling project namespaces.
# Remove it once the back-filling of all project namespaces is done.
@ -660,6 +661,10 @@ class Namespace < ApplicationRecord
# Use SHA2 of `traversal_ids` to account for moving a namespace within the same root ancestor hierarchy.
"namespaces:{#{traversal_ids.first}}:first_auto_devops_config:#{group_id}:#{Digest::SHA2.hexdigest(traversal_ids.join(' '))}"
end
def allow_serialization?(options = nil)
Feature.disabled?(:block_namespace_serialization, self, default_enabled: :yaml) || super
end
end
Namespace.prepend_mod_with('Namespace')

View File

@ -9,10 +9,5 @@ module IncidentManagement
track_usage_event(:"incident_management_#{action}", current_user.id)
end
# No-op as optionally overridden in implementing classes.
# For use to provide checks before calling #track_incident_action.
def track_event
end
end
end

View File

@ -25,10 +25,15 @@ module Groups
private
def proceed_to_transfer
old_root_ancestor_id = @group.root_ancestor.id
was_root_group = @group.root?
Group.transaction do
update_group_attributes
ensure_ownership
update_integrations
remove_issue_contacts(old_root_ancestor_id, was_root_group)
update_crm_objects(was_root_group)
end
post_update_hooks(@updated_project_ids)
@ -53,6 +58,17 @@ module Groups
raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup?
raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages?
raise_transfer_error(:no_permissions_to_migrate_crm) if no_permissions_to_migrate_crm?
end
def no_permissions_to_migrate_crm?
return false unless group && @new_parent_group
return false if group.root_ancestor == @new_parent_group.root_ancestor
return true if group.contacts.exists? && !current_user.can?(:admin_crm_contact, @new_parent_group.root_ancestor)
return true if group.organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor)
false
end
def group_with_npm_packages?
@ -202,7 +218,8 @@ module Groups
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'),
cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'),
group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.')
group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.'),
no_permissions_to_migrate_crm: s_("TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
}.freeze
end
@ -238,6 +255,20 @@ module Groups
namespace_id: group.id
}
end
def update_crm_objects(was_root_group)
return unless was_root_group
CustomerRelations::Contact.move_to_root_group(group)
CustomerRelations::Organization.move_to_root_group(group)
end
def remove_issue_contacts(old_root_ancestor_id, was_root_group)
return if was_root_group
return if old_root_ancestor_id == @group.root_ancestor.id
CustomerRelations::IssueContact.delete_for_group(@group)
end
end
end

View File

@ -2,8 +2,6 @@
module IssuableLinks
class CreateService < BaseService
include IncidentManagement::UsageData
attr_reader :issuable, :current_user, :params
def initialize(issuable, user, params)
@ -25,7 +23,7 @@ module IssuableLinks
end
@errors = []
create_links
references = create_links
if @errors.present?
return error(@errors.join('. '), 422)
@ -33,7 +31,7 @@ module IssuableLinks
track_event
success
success(created_references: references)
end
# rubocop: disable CodeReuse/ActiveRecord
@ -66,7 +64,7 @@ module IssuableLinks
end
def link_issuables(target_issuables)
target_issuables.each do |referenced_object|
target_issuables.map do |referenced_object|
link = relate_issuables(referenced_object)
unless link.valid?
@ -75,6 +73,8 @@ module IssuableLinks
error: link.errors.messages.values.flatten.to_sentence
}
end
link
end
end
@ -142,6 +142,10 @@ module IssuableLinks
def set_link_type(_link)
# no-op
end
def track_event
# no-op
end
end
end

View File

@ -2,8 +2,6 @@
module IssuableLinks
class DestroyService < BaseService
include IncidentManagement::UsageData
attr_reader :link, :current_user, :source, :target
def initialize(link, user)
@ -41,5 +39,9 @@ module IssuableLinks
def not_found_message
'No Issue Link found'
end
def track_event
# no op
end
end
end

View File

@ -2,6 +2,8 @@
module IssueLinks
class CreateService < IssuableLinks::CreateService
include IncidentManagement::UsageData
def linkable_issuables(issues)
@linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }

View File

@ -2,6 +2,8 @@
module IssueLinks
class DestroyService < IssuableLinks::DestroyService
include IncidentManagement::UsageData
private
def permission_to_remove_relation?

View File

@ -111,7 +111,7 @@ module Notes
def track_event(note, user)
track_note_creation_usage_for_issues(note) if note.for_issue?
track_note_creation_usage_for_merge_requests(note) if note.for_merge_request?
track_usage_event(:incident_management_incident_comment, user.id) if note.for_issue? && note.noteable.incident?
track_incident_action(user, note.noteable, 'incident_comment') if note.for_issue?
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note))

View File

@ -27,7 +27,7 @@
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s90')
= render 'shared/choose_avatar_button', f: f
- if @topic.avatar?
.js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic) } }
.js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic), name: @topic.name } }
- if @topic.new_record?
.form-actions

View File

@ -1,3 +1,3 @@
= form_for [@project, @issue],
html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors' } do |f|
html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors', data: issues_form_data(@project) } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue

View File

@ -0,0 +1,8 @@
---
name: block_namespace_serialization
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82661
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355553
milestone: '14.9'
type: development
group: group::global search
default_enabled: false

View File

@ -29,7 +29,7 @@ class Gitlab::Seeder::Crm
group_id: group.id,
first_name: first_name,
last_name: last_name,
email: "#{first_name}.#{last_name}@example.org",
email: "#{first_name}.#{last_name}-#{index}@example.org",
organization_id: organization_id
)

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class CreateProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
def up
create_table :protected_environment_approval_rules do |t|
t.references :protected_environment,
index: { name: :index_approval_rule_on_protected_environment_id },
foreign_key: { to_table: :protected_environments, on_delete: :cascade },
null: false
t.bigint :user_id
t.bigint :group_id
t.timestamps_with_timezone null: false
t.integer :access_level, limit: 2
t.integer :required_approvals, null: false, limit: 2
t.index :user_id
t.index :group_id
t.check_constraint "((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR " \
"((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR " \
"((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL))"
t.check_constraint "required_approvals > 0"
end
end
def down
drop_table :protected_environment_approval_rules
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddUserFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :protected_environment_approval_rules, :users, column: :user_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key_if_exists :protected_environment_approval_rules, column: :user_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddGroupFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :protected_environment_approval_rules, :namespaces, column: :group_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key_if_exists :protected_environment_approval_rules, column: :group_id
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddApprovalRuleIdToDeploymentApprovals < Gitlab::Database::Migration[1.0]
def change
add_column :deployment_approvals, :approval_rule_id, :bigint
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddApprovalRuleFkToDeploymentApprovals < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_deployment_approvals_on_approval_rule_id'
def up
add_concurrent_index :deployment_approvals, :approval_rule_id, name: INDEX_NAME
add_concurrent_foreign_key :deployment_approvals, :protected_environment_approval_rules, column: :approval_rule_id, on_delete: :nullify
end
def down
with_lock_retries do
remove_foreign_key_if_exists :deployment_approvals, column: :approval_rule_id
end
remove_concurrent_index_by_name :deployment_approvals, INDEX_NAME
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class UpdateOrganizationsNameIndexAddId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
OLD_INDEX = 'index_customer_relations_organizations_on_unique_name_per_group'
NEW_INDEX = 'index_organizations_on_unique_name_per_group'
def up
add_concurrent_index :customer_relations_organizations, 'group_id, lower(name), id', name: NEW_INDEX, unique: true
remove_concurrent_index_by_name :customer_relations_organizations, OLD_INDEX
end
def down
add_concurrent_index :customer_relations_organizations, 'group_id, lower(name)', name: OLD_INDEX, unique: true
remove_concurrent_index_by_name :customer_relations_organizations, NEW_INDEX
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddContactsIndexOnGroupEmailAndId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_customer_relations_contacts_on_unique_email_per_group'
def up
add_concurrent_index :customer_relations_contacts, 'group_id, lower(email), id', name: INDEX_NAME, unique: true
end
def down
remove_concurrent_index_by_name :customer_relations_contacts, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
258c7a3409aea1c713c2ddd6679de586e7548ce4d7c0811db1d4903f2794c660

View File

@ -0,0 +1 @@
85be80bb8c929d017fedfe66c1f18e4a0dbd7dd8f3b683ded60213e621ec06f4

View File

@ -0,0 +1 @@
41e7a36164fe3b1b582149d9cfbefc6ee2ce804ac85207f918c064b0ef738b53

View File

@ -0,0 +1 @@
e58b89906cd09577c1a773768e4cf3f97b870744e4ee6a323e0276895dff0de5

View File

@ -0,0 +1 @@
e2fa0265f3c14c8e6f08a4ffc4b682d8805fa634bac66c578684faaee97541cf

View File

@ -0,0 +1 @@
659accb8efe0223028a74346ecf3aa5b649cda825ac7941bc932bc1d7e6f8d9a

View File

@ -0,0 +1 @@
d24c5a5414e44385a132e8f342cb67cc5a7c5fe4bfcc4c15c473397076350bc2

View File

@ -14270,6 +14270,7 @@ CREATE TABLE deployment_approvals (
updated_at timestamp with time zone NOT NULL,
status smallint NOT NULL,
comment text,
approval_rule_id bigint,
CONSTRAINT check_e2eb6a17d8 CHECK ((char_length(comment) <= 255))
);
@ -19681,6 +19682,28 @@ CREATE SEQUENCE protected_branches_id_seq
ALTER SEQUENCE protected_branches_id_seq OWNED BY protected_branches.id;
CREATE TABLE protected_environment_approval_rules (
id bigint NOT NULL,
protected_environment_id bigint NOT NULL,
user_id bigint,
group_id bigint,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
access_level smallint,
required_approvals smallint NOT NULL,
CONSTRAINT chk_rails_bed75249bc CHECK ((((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR ((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR ((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL)))),
CONSTRAINT chk_rails_cfa90ae3b5 CHECK ((required_approvals > 0))
);
CREATE SEQUENCE protected_environment_approval_rules_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE protected_environment_approval_rules_id_seq OWNED BY protected_environment_approval_rules.id;
CREATE TABLE protected_environment_deploy_access_levels (
id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -22950,6 +22973,8 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels ALTER COLUMN id SET DE
ALTER TABLE ONLY protected_branches ALTER COLUMN id SET DEFAULT nextval('protected_branches_id_seq'::regclass);
ALTER TABLE ONLY protected_environment_approval_rules ALTER COLUMN id SET DEFAULT nextval('protected_environment_approval_rules_id_seq'::regclass);
ALTER TABLE ONLY protected_environment_deploy_access_levels ALTER COLUMN id SET DEFAULT nextval('protected_environment_deploy_access_levels_id_seq'::regclass);
ALTER TABLE ONLY protected_environments ALTER COLUMN id SET DEFAULT nextval('protected_environments_id_seq'::regclass);
@ -25066,6 +25091,9 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels
ALTER TABLE ONLY protected_branches
ADD CONSTRAINT protected_branches_pkey PRIMARY KEY (id);
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT protected_environment_approval_rules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY protected_environment_deploy_access_levels
ADD CONSTRAINT protected_environment_deploy_access_levels_pkey PRIMARY KEY (id);
@ -26707,6 +26735,8 @@ CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON approv
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
CREATE INDEX index_approval_rule_on_protected_environment_id ON protected_environment_approval_rules USING btree (protected_environment_id);
CREATE INDEX index_approval_rules_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2);
CREATE INDEX index_approvals_on_merge_request_id ON approvals USING btree (merge_request_id);
@ -27273,7 +27303,7 @@ CREATE INDEX index_customer_relations_contacts_on_group_id ON customer_relations
CREATE INDEX index_customer_relations_contacts_on_organization_id ON customer_relations_contacts USING btree (organization_id);
CREATE UNIQUE INDEX index_customer_relations_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name));
CREATE UNIQUE INDEX index_customer_relations_contacts_on_unique_email_per_group ON customer_relations_contacts USING btree (group_id, lower(email), id);
CREATE UNIQUE INDEX index_cycle_analytics_stage_event_hashes_on_hash_sha_256 ON analytics_cycle_analytics_stage_event_hashes USING btree (hash_sha256);
@ -27343,6 +27373,8 @@ CREATE INDEX index_deploy_tokens_on_token_and_expires_at_and_id ON deploy_tokens
CREATE UNIQUE INDEX index_deploy_tokens_on_token_encrypted ON deploy_tokens USING btree (token_encrypted);
CREATE INDEX index_deployment_approvals_on_approval_rule_id ON deployment_approvals USING btree (approval_rule_id);
CREATE UNIQUE INDEX index_deployment_approvals_on_deployment_id_and_user_id ON deployment_approvals USING btree (deployment_id, user_id);
CREATE INDEX index_deployment_approvals_on_user_id ON deployment_approvals USING btree (user_id);
@ -28393,6 +28425,8 @@ CREATE UNIQUE INDEX index_ops_feature_flags_issues_on_feature_flag_id_and_issue_
CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list_id ON operations_strategies_user_lists USING btree (strategy_id, user_list_id);
CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id);
CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
CREATE INDEX index_packages_build_infos_package_id_pipeline_id ON packages_build_infos USING btree (package_id, pipeline_id);
@ -28785,6 +28819,10 @@ CREATE INDEX index_protected_branch_unprotect_access_levels_on_user_id ON protec
CREATE INDEX index_protected_branches_on_project_id ON protected_branches USING btree (project_id);
CREATE INDEX index_protected_environment_approval_rules_on_group_id ON protected_environment_approval_rules USING btree (group_id);
CREATE INDEX index_protected_environment_approval_rules_on_user_id ON protected_environment_approval_rules USING btree (user_id);
CREATE INDEX index_protected_environment_deploy_access ON protected_environment_deploy_access_levels USING btree (protected_environment_id);
CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON protected_environment_deploy_access_levels USING btree (group_id);
@ -31170,6 +31208,9 @@ ALTER TABLE ONLY ci_pipelines
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_3d674b9f23 FOREIGN KEY (updated_state_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_schedule_variables
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
@ -31233,6 +31274,9 @@ ALTER TABLE ONLY project_access_tokens
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY deployment_approvals
ADD CONSTRAINT fk_61cdbdc5b9 FOREIGN KEY (approval_rule_id) REFERENCES protected_environment_approval_rules(id) ON DELETE SET NULL;
ALTER TABLE ONLY dast_profile_schedules
ADD CONSTRAINT fk_61d52aa0e7 FOREIGN KEY (dast_profile_id) REFERENCES dast_profiles(id) ON DELETE CASCADE;
@ -31272,6 +31316,9 @@ ALTER TABLE ONLY projects
ALTER TABLE ONLY terraform_state_versions
ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT fk_6ee8249821 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY protected_branch_push_access_levels
ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -32283,6 +32330,9 @@ ALTER TABLE ONLY scim_identities
ALTER TABLE ONLY snippet_user_mentions
ADD CONSTRAINT fk_rails_4d3f96b2cb FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT fk_rails_4e554f96f5 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE;
ALTER TABLE ONLY deployment_clusters
ADD CONSTRAINT fk_rails_4e6243e120 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Support for reStructuredText and Textile documents [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324766) in GitLab 13.12.
When [Kroki](https://kroki.io) integration is enabled and configured in
GitLab you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
GitLab, you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
## Kroki Server

View File

@ -88,3 +88,120 @@ Example response:
}
]
```
## Create a related epic link
Create a two-way relation between two epics. The user must be allowed to
update both epics to succeed.
```plaintext
POST /groups/:id/epics/:epic_iid/related_epics
```
Supported attributes:
| Attribute | Type | Required | Description |
|---------------------|----------------|-----------------------------|---------------------------------------|
| `epic_iid` | integer | **{check-circle}** Yes | Internal ID of a group's epic. |
| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `target_epic_iid` | integer/string | **{check-circle}** Yes | Internal ID of a target group's epic. |
| `target_group_id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the target group](index.md#namespaced-path-encoding). |
| `link_type` | string | **{dotted-circle}** No | Type of the relation (`relates_to`, `blocks`, `is_blocked_by`), defaults to `relates_to`. |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/26/epics/1/related_epics?target_group_id=26&target_epic_iid=5"
```
Example response:
```json
{
"source_epic": {
"id": 21,
"iid": 1,
"color": "#1068bf",
"text_color": "#FFFFFF",
"group_id": 26,
"parent_id": null,
"parent_iid": null,
"title": "Aspernatur recusandae distinctio omnis et qui est iste.",
"description": "some description",
"confidential": false,
"author": {
"id": 15,
"username": "trina",
"name": "Theresia Robel",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/085e28df717e16484cbf6ceca75e9a93?s=80&d=identicon",
"web_url": "http://gitlab.example.com/trina"
},
"start_date": null,
"end_date": null,
"due_date": null,
"state": "opened",
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/1",
"references": {
"short": "&1",
"relative": "&1",
"full": "flightjs&1"
},
"created_at": "2022-01-31T15:10:44.988Z",
"updated_at": "2022-03-16T09:32:35.712Z",
"closed_at": null,
"labels": [],
"upvotes": 0,
"downvotes": 0,
"_links": {
"self": "http://gitlab.example.com/api/v4/groups/26/epics/1",
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/1/issues",
"group": "http://gitlab.example.com/api/v4/groups/26",
"parent": null
}
},
"target_epic": {
"id": 25,
"iid": 5,
"color": "#1068bf",
"text_color": "#FFFFFF",
"group_id": 26,
"parent_id": null,
"parent_iid": null,
"title": "Aut assumenda id nihil distinctio fugiat vel numquam est.",
"description": "some description",
"confidential": false,
"author": {
"id": 3,
"username": "valerie",
"name": "Erika Wolf",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/9ef7666abb101418a4716a8ed4dded80?s=80&d=identicon",
"web_url": "http://gitlab.example.com/valerie"
},
"start_date": null,
"end_date": null,
"due_date": null,
"state": "opened",
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/5",
"references": {
"short": "&5",
"relative": "&5",
"full": "flightjs&5"
},
"created_at": "2022-01-31T15:10:45.080Z",
"updated_at": "2022-03-16T09:32:35.842Z",
"closed_at": null,
"labels": [],
"upvotes": 0,
"downvotes": 0,
"_links": {
"self": "http://gitlab.example.com/api/v4/groups/26/epics/5",
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/5/issues",
"group": "http://gitlab.example.com/api/v4/groups/26",
"parent": null
}
},
"link_type": "relates_to"
}
```

View File

@ -99,6 +99,8 @@ GET /users?exclude_external=true
### For admins
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
```plaintext
GET /users
```
@ -151,7 +153,8 @@ GET /users
"external": false,
"private_profile": false,
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22"
"last_sign_in_ip": "172.127.2.22",
"namespace_id": 1
},
{
"id": 2,
@ -185,7 +188,8 @@ GET /users
"external": false,
"private_profile": false,
"current_sign_in_ip": "10.165.1.102",
"last_sign_in_ip": "172.127.2.22"
"last_sign_in_ip": "172.127.2.22",
"namespace_id": 2
}
]
```
@ -300,6 +304,8 @@ Parameters:
### For admin
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
```plaintext
GET /users/:id
```
@ -355,7 +361,8 @@ Example Responses:
"last_sign_in_ip": "172.127.2.22",
"plan": "gold",
"trial": true,
"sign_in_count": 1337
"sign_in_count": 1337,
"namespace_id": 1
}
```
@ -404,6 +411,8 @@ GET /users/:id?with_custom_attributes=true
## User creation
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
Creates a new user. Note only administrators can create new
users. Either `password`, `reset_password`, or `force_random_password`
must be specified. If `reset_password` and `force_random_password` are
@ -459,6 +468,8 @@ Parameters:
## User modification
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
Modifies an existing user. Only administrators can change attributes of a user.
```plaintext
@ -583,6 +594,8 @@ GET /user
## List current user (for admins)
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
```plaintext
GET /user
```
@ -632,7 +645,8 @@ Parameters:
"private_profile": false,
"commit_email": "john-codes@example.com",
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22"
"last_sign_in_ip": "172.127.2.22",
"namespace_id": 1
}
```

View File

@ -598,6 +598,9 @@ To upload payload manually:
1. Select **Choose file** and choose the file from p5.
1. Select **Upload**.
The uploaded file is encrypted and sent using secure [HTTPS protocol](https://en.wikipedia.org/wiki/HTTPS). HTTPS creates a secure
communication channel between web browser and the server, and protects transmitted data against man-in-the-middle attacks.
## Monitoring
Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health).

View File

@ -5,6 +5,7 @@ module API
class UserWithAdmin < UserPublic
expose :admin?, as: :is_admin
expose :note
expose :namespace_id
end
end
end

View File

@ -120,8 +120,11 @@ module API
users = reorder_users(users)
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
if entity == Entities::UserWithAdmin
users = users.preload(:identities, :u2f_registrations, :webauthn_registrations, :namespace)
end
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
users = users.preload(:user_detail)

View File

@ -435,6 +435,7 @@ protected_branches: :gitlab_main
protected_branch_merge_access_levels: :gitlab_main
protected_branch_push_access_levels: :gitlab_main
protected_branch_unprotect_access_levels: :gitlab_main
protected_environment_approval_rules: :gitlab_main
protected_environment_deploy_access_levels: :gitlab_main
protected_environments: :gitlab_main
protected_tag_create_access_levels: :gitlab_main

View File

@ -92,7 +92,7 @@ module Gitlab
def simple_serialize
subject.as_json(
tree.merge(include: nil, preloads: nil))
tree.merge(include: nil, preloads: nil, unsafe: true))
end
def serialize_includes

View File

@ -37,7 +37,7 @@ module Gitlab
def serialize_root(exportable_path = @exportable_path)
attributes = exportable.as_json(
relations_schema.merge(include: nil, preloads: nil))
relations_schema.merge(include: nil, preloads: nil, unsafe: true))
json_writer.write_attributes(exportable_path, attributes)
end

View File

@ -27,6 +27,11 @@ module Mattermost
LEASE_TIMEOUT = 60
Request = Struct.new(:parameters, keyword_init: true) do
def method_missing(method_name, *args, &block)
end
end
attr_accessor :current_resource_owner, :token, :base_uri
def initialize(current_user)
@ -64,7 +69,7 @@ module Mattermost
end
def request
@request ||= OpenStruct.new(parameters: params)
@request ||= Request.new(parameters: params)
end
def params

View File

@ -9424,9 +9424,6 @@ msgstr ""
msgid "Confirm new password"
msgstr ""
msgid "Confirm remove avatar"
msgstr ""
msgid "Confirm user"
msgstr ""
@ -22703,6 +22700,9 @@ msgstr ""
msgid "Manual"
msgstr ""
msgid "Manual iteration cadences are deprecated. Only automatic iteration cadences are allowed."
msgstr ""
msgid "ManualOrdering|Couldn't save the order of the issues"
msgstr ""
@ -30947,6 +30947,9 @@ msgstr ""
msgid "Remove time estimate"
msgstr ""
msgid "Remove topic avatar"
msgstr ""
msgid "Remove user"
msgstr ""
@ -39156,6 +39159,9 @@ msgstr ""
msgid "Topic avatar"
msgstr ""
msgid "Topic avatar for %{name} will be removed. This cannot be undone."
msgstr ""
msgid "Topic name"
msgstr ""
@ -39249,6 +39255,9 @@ msgstr ""
msgid "TransferGroup|Database is not supported."
msgstr ""
msgid "TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group."
msgstr ""
msgid "TransferGroup|Group contains projects with NPM packages."
msgstr ""

View File

@ -56,11 +56,8 @@ module DeprecationToolkitEnv
# In this case, we recommend to add a silence together with an issue to patch or update
# the dependency causing the problem.
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
#
# - ruby/lib/grpc/generic/interceptors.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/339305
def self.allowed_kwarg_warning_paths
%w[
ruby/lib/grpc/generic/interceptors.rb
]
end

View File

@ -26,7 +26,8 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
"external"
"external",
"namespace_id"
],
"properties": {
"$ref": "full.json"

View File

@ -1,10 +1,11 @@
import { GlButton, GlModal } from '@gitlab/ui';
import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveAvatar from '~/admin/topics/components/remove_avatar.vue';
const modalID = 'fake-id';
const path = 'topic/path/1';
const name = 'Topic 1';
jest.mock('lodash/uniqueId', () => () => 'fake-id');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@ -16,10 +17,14 @@ describe('RemoveAvatar', () => {
wrapper = shallowMount(RemoveAvatar, {
provide: {
path,
name,
},
directives: {
GlModal: createMockDirective(),
},
stubs: {
GlSprintf,
},
});
};
@ -55,8 +60,8 @@ describe('RemoveAvatar', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
expect(modal.props('title')).toBe('Confirm remove avatar');
expect(modal.text()).toBe('Avatar will be removed. Are you sure?');
expect(modal.props('title')).toBe('Remove topic avatar');
expect(modal.text()).toBe(`Topic avatar for ${name} will be removed. This cannot be undone.`);
});
it('contains the correct modal ID', () => {

View File

@ -1,20 +1,46 @@
import $ from 'jquery';
import IssuableForm from '~/issuable/issuable_form';
function createIssuable() {
const instance = new IssuableForm($(document.createElement('form')));
instance.titleField = $(document.createElement('input'));
return instance;
}
import setWindowLocation from 'helpers/set_window_location_helper';
describe('IssuableForm', () => {
let instance;
const createIssuable = (form) => {
instance = new IssuableForm(form);
};
beforeEach(() => {
instance = createIssuable();
setFixtures(`
<form>
<input name="[title]" />
</form>
`);
createIssuable($('form'));
});
describe('initAutosave', () => {
it('creates autosave with the searchTerm included', () => {
setWindowLocation('https://gitlab.test/foo?bar=true');
const autosave = instance.initAutosave();
expect(autosave.key.includes('bar=true')).toBe(true);
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
setFixtures(`
<form data-new-issue-path="/issues/new">
<input name="[title]" />
</form>
`);
createIssuable($('form'));
setWindowLocation('https://gitlab.test/issues/new?bar=true');
const autosave = instance.initAutosave();
expect(autosave.key.includes('bar=true')).toBe(false);
});
});
describe('removeWip', () => {

View File

@ -368,6 +368,16 @@ RSpec.describe IssuesHelper do
end
end
describe '#issues_form_data' do
it 'returns expected result' do
expected = {
new_issue_path: new_project_issue_path(project)
}
expect(helper.issues_form_data(project)).to include(expected)
end
end
describe '#issue_manual_ordering_class' do
context 'when sorting by relative position' do
before do

View File

@ -665,6 +665,7 @@ protected_environments:
- project
- group
- deploy_access_levels
- approval_rules
deploy_access_levels:
- protected_environment
- user

View File

@ -35,6 +35,12 @@ RSpec.describe Mattermost::Session, type: :request do
it 'makes a request to the oauth uri' do
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
it 'returns nill on calling a non exisitng method on request' do
return_value = subject.request.method_missing("non_existing_method", "something") do
end
expect(return_value).to be(nil)
end
end
context 'with oauth_uri' do

View File

@ -25,7 +25,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it { is_expected.to validate_length_of(:email).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
it { is_expected.to validate_uniqueness_of(:email).scoped_to(:group_id) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive.scoped_to(:group_id) }
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
end
@ -87,6 +87,15 @@ RSpec.describe CustomerRelations::Contact, type: :model do
too_many_emails = described_class::MAX_PLUCK + 1
expect { described_class.find_ids_by_emails(group, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
end
it 'finds contacts regardless of email casing' do
new_contact = create(:contact, group: group, email: "UPPERCASE@example.com")
emails = [group_contacts[0].email.downcase, group_contacts[1].email.upcase, new_contact.email]
contact_ids = described_class.find_ids_by_emails(group, emails)
expect(contact_ids).to contain_exactly(group_contacts[0].id, group_contacts[1].id, new_contact.id)
end
end
describe '#self.exists_for_group?' do
@ -104,4 +113,33 @@ RSpec.describe CustomerRelations::Contact, type: :model do
end
end
end
describe '#self.move_to_root_group' do
let!(:old_root_group) { create(:group) }
let!(:contacts) { create_list(:contact, 4, group: old_root_group) }
let!(:project) { create(:project, group: old_root_group) }
let!(:issue) { create(:issue, project: project) }
let!(:issue_contact1) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]) }
let!(:issue_contact2) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]) }
let!(:new_root_group) { create(:group) }
let!(:dupe_contact1) { create(:contact, group: new_root_group, email: contacts[1].email) }
let!(:dupe_contact2) { create(:contact, group: new_root_group, email: contacts[3].email.upcase) }
before do
old_root_group.update!(parent: new_root_group)
CustomerRelations::Contact.move_to_root_group(old_root_group)
end
it 'moves contacts with unique emails and deletes the rest' do
expect(contacts[0].reload.group_id).to eq(new_root_group.id)
expect(contacts[2].reload.group_id).to eq(new_root_group.id)
expect { contacts[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { contacts[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'updates issue_contact.contact_id for dupes and leaves the rest untouched' do
expect(issue_contact1.reload.contact_id).to eq(contacts[0].id)
expect(issue_contact2.reload.contact_id).to eq(dupe_contact1.id)
end
end
end

View File

@ -92,4 +92,16 @@ RSpec.describe CustomerRelations::IssueContact do
expect { described_class.delete_for_project(project.id) }.to change { described_class.count }.by(-3)
end
end
describe '.delete_for_group' do
let(:project_for_root_group) { create(:project, group: group) }
it 'destroys all issue_contacts for projects in group and subgroups' do
create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project))
create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project_for_root_group))
create(:issue_customer_relations_contact)
expect { described_class.delete_for_group(group) }.to change { described_class.count }.by(-4)
end
end
end

View File

@ -50,4 +50,32 @@ RSpec.describe CustomerRelations::Organization, type: :model do
expect(described_class.find_by_name(group.id, 'TEST')).to eq([organiztion1])
end
end
describe '#self.move_to_root_group' do
let!(:old_root_group) { create(:group) }
let!(:organizations) { create_list(:organization, 4, group: old_root_group) }
let!(:new_root_group) { create(:group) }
let!(:contact1) { create(:contact, group: new_root_group, organization: organizations[0]) }
let!(:contact2) { create(:contact, group: new_root_group, organization: organizations[1]) }
let!(:dupe_organization1) { create(:organization, group: new_root_group, name: organizations[1].name) }
let!(:dupe_organization2) { create(:organization, group: new_root_group, name: organizations[3].name.upcase) }
before do
old_root_group.update!(parent: new_root_group)
CustomerRelations::Organization.move_to_root_group(old_root_group)
end
it 'moves organizations with unique names and deletes the rest' do
expect(organizations[0].reload.group_id).to eq(new_root_group.id)
expect(organizations[2].reload.group_id).to eq(new_root_group.id)
expect { organizations[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { organizations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'updates contact.organization_id for dupes and leaves the rest untouched' do
expect(contact1.reload.organization_id).to eq(organizations[0].id)
expect(contact2.reload.organization_id).to eq(dupe_organization1.id)
end
end
end

View File

@ -1021,4 +1021,54 @@ RSpec.describe Integration do
)
end
end
describe 'boolean_accessor' do
let(:klass) do
Class.new(Integration) do
boolean_accessor :test_value
end
end
let(:integration) { klass.new(properties: { test_value: input }) }
where(:input, :method_result, :predicate_method_result) do
true | true | true
false | false | false
1 | true | true
0 | false | false
'1' | true | true
'0' | false | false
'true' | true | true
'false' | false | false
'foobar' | nil | false
'' | nil | false
nil | nil | false
'on' | true | true
'off' | false | false
'yes' | true | true
'no' | false | false
'n' | false | false
'y' | true | true
't' | true | true
'f' | false | false
end
with_them do
it 'has the correct value' do
expect(integration).to have_attributes(
test_value: be(method_result),
test_value?: be(predicate_method_result)
)
end
end
it 'returns values when initialized without input' do
integration = klass.new
expect(integration).to have_attributes(
test_value: be(nil),
test_value?: be(false)
)
end
end
end

View File

@ -2230,4 +2230,18 @@ RSpec.describe Namespace do
expect(namespace.storage_enforcement_date).to be(nil)
end
end
describe 'serialization' do
let(:object) { build(:namespace) }
it_behaves_like 'blocks unsafe serialization'
context 'when feature flag block_namespace_serialization is disabled' do
before do
stub_feature_flags(block_namespace_serialization: false)
end
it_behaves_like 'allows unsafe serialization'
end
end
end

View File

@ -83,19 +83,21 @@ RSpec.describe API::Users do
describe 'GET /users/' do
context 'when unauthenticated' do
it "does not contain the note of users" do
it "does not contain certain fields" do
get api("/users"), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
expect(json_response.first).not_to have_key('namespace_id')
end
end
context 'when authenticated' do
context 'as a regular user' do
it 'does not contain the note of users' do
it 'does not contain certain fields' do
get api("/users", user), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
expect(json_response.first).not_to have_key('namespace_id')
end
end
@ -154,6 +156,7 @@ RSpec.describe API::Users do
get api("/user", user)
expect(json_response).not_to have_key('note')
expect(json_response).not_to have_key('namespace_id')
end
end
end
@ -384,6 +387,15 @@ RSpec.describe API::Users do
expect(response).to include_pagination_headers
end
it "users contain the `namespace_id` field" do
get api("/users", admin)
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['namespace_id'] }).to include(user.namespace_id, admin.namespace_id)
end
it "returns an array of external users" do
create(:user, external: true)
@ -697,6 +709,14 @@ RSpec.describe API::Users do
expect(json_response['highest_role']).to be(0)
end
it 'includes the `namespace_id` field' do
get api("/users/#{user.id}", admin)
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['namespace_id']).to eq(user.namespace_id)
end
if Gitlab.ee?
it 'does not include values for plan or trial' do
get api("/users/#{user.id}", admin)

View File

@ -17,7 +17,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
let_it_be(:new_parent_group) { create(:group, :public, :crm_enabled) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
@ -876,5 +876,108 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
end
end
context 'crm' do
let(:root_group) { create(:group, :public) }
let(:subgroup) { create(:group, :public, parent: root_group) }
let(:another_subgroup) { create(:group, :public, parent: root_group) }
let(:subsubgroup) { create(:group, :public, parent: subgroup) }
let(:root_project) { create(:project, group: root_group) }
let(:sub_project) { create(:project, group: subgroup) }
let(:another_project) { create(:project, group: another_subgroup) }
let(:subsub_project) { create(:project, group: subsubgroup) }
let!(:contacts) { create_list(:contact, 4, group: root_group) }
let!(:organizations) { create_list(:organization, 2, group: root_group) }
before do
create(:issue_customer_relations_contact, contact: contacts[0], issue: create(:issue, project: root_project))
create(:issue_customer_relations_contact, contact: contacts[1], issue: create(:issue, project: sub_project))
create(:issue_customer_relations_contact, contact: contacts[2], issue: create(:issue, project: another_project))
create(:issue_customer_relations_contact, contact: contacts[3], issue: create(:issue, project: subsub_project))
root_group.add_owner(user)
end
context 'moving up' do
let(:group) { subsubgroup }
it 'retains issue contacts' do
expect { transfer_service.execute(root_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
end
context 'moving down' do
let(:group) { subgroup }
it 'retains issue contacts' do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
end
context 'moving sideways' do
let(:group) { subsubgroup }
it 'retains issue contacts' do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
end
context 'moving to new root group' do
let(:group) { root_group }
before do
new_parent_group.add_owner(user)
end
it 'moves all crm objects' do
expect { transfer_service.execute(new_parent_group) }
.to change { root_group.contacts.count }.by(-4)
.and change { root_group.organizations.count }.by(-2)
end
it 'retains issue contacts' do
expect { transfer_service.execute(new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
end
context 'moving to a subgroup within a new root group' do
let(:group) { root_group }
let(:subgroup_in_new_parent_group) { create(:group, parent: new_parent_group) }
context 'with permission on the root group' do
before do
new_parent_group.add_owner(user)
end
it 'moves all crm objects' do
expect { transfer_service.execute(subgroup_in_new_parent_group) }
.to change { root_group.contacts.count }.by(-4)
.and change { root_group.organizations.count }.by(-2)
end
it 'retains issue contacts' do
expect { transfer_service.execute(subgroup_in_new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
end
context 'with permission on the subgroup' do
before do
subgroup_in_new_parent_group.add_owner(user)
end
it 'raises error' do
transfer_service.execute(subgroup_in_new_parent_group)
expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
end
end
end
end
end
end

View File

@ -70,8 +70,10 @@ shared_examples 'issuable link creation' do
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
end
it 'returns success status' do
is_expected.to eq(status: :success)
it 'returns success status and created links', :aggregate_failures do
expect(subject.keys).to match_array([:status, :created_references])
expect(subject[:status]).to eq(:success)
expect(subject[:created_references].map(&:target_id)).to match_array([issuable2.id, issuable3.id])
end
it 'creates notes' do