Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-04-19 09:13:38 +00:00
parent 98decb267a
commit 92d681f6f0
58 changed files with 772 additions and 295 deletions

View File

@ -32,7 +32,7 @@ prepare-as-if-foss-branch:
- git add -A
# --allow-empty accounts for the edge case where FOSS matchess EE repository
# and a merge request only contains EE related changes.
- git commit -m 'Update from merge request' --allow-empty # TODO: Mark which SHA we add
- git commit -m 'Update from merge request' --allow-empty # TODO: Mark which SHA we add
- git push -f "${FOSS_REPOSITORY}" "${AS_IF_FOSS_BRANCH}"
prepare-as-if-foss-env:

View File

@ -73,7 +73,7 @@ compile-test-assets:
expire_in: 7d
paths:
- public/assets/
- config/helpers/tailwind/ # Assets created during tailwind compilation
- config/helpers/tailwind/ # Assets created during tailwind compilation
- "${WEBPACK_COMPILE_LOG_PATH}"
when: always

View File

@ -308,7 +308,7 @@
.ai-gateway-variables:
variables:
AIGW_AUTH__BYPASS_EXTERNAL: True
AIGW_AUTH__BYPASS_EXTERNAL: true
AIGW_VERTEX_TEXT_MODEL__PROJECT: $VERTEX_AI_PROJECT
AIGW_VERTEX_TEXT_MODEL__JSON_KEY: $VERTEX_AI_CREDENTIALS
AIGW_FASTAPI__DOCS_URL: "/docs"

View File

@ -1393,7 +1393,7 @@
changes: *frontend-dependency-patterns
when: never
- <<: *if-merge-request
changes: [".gitlab/ci/frontend.gitlab-ci.yml"] # When this file is modified, we run full Jest jobs
changes: [".gitlab/ci/frontend.gitlab-ci.yml"] # When this file is modified, we run full Jest jobs
when: never
- <<: *if-merge-request
changes: *frontend-predictive-patterns
@ -2855,7 +2855,7 @@
.setup:rules:set-pipeline-name:
rules:
- <<: *if-not-merge-request # This is only designed to run in a merge request
- <<: *if-not-merge-request # This is only designed to run in a merge request
when: never
- if: '$PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE == null'
when: never

View File

@ -18,11 +18,11 @@ spec:
- ".gitlab/ci/gitlab-gems.gitlab-ci.yml"
- ".gitlab/ci/vendored-gems.gitlab-ci.yml"
- ".gitlab/ci/templates/gem.gitlab-ci.yml"
# Ensure dependency updates don't fail child pipelines: https://gitlab.com/gitlab-org/gitlab/-/issues/417428
# Ensure dependency updates don't fail child pipelines: https://gitlab.com/gitlab-org/gitlab/-/issues/417428
- "Gemfile.lock"
- "gems/gem.gitlab-ci.yml"
- "gems/gem-pg.gitlab-ci.yml"
# Ensure new cop in the monolith don't break internal gems Rubocop checks: https://gitlab.com/gitlab-org/gitlab/-/issues/419915
# Ensure new cop in the monolith don't break internal gems Rubocop checks: https://gitlab.com/gitlab-org/gitlab/-/issues/419915
- ".rubocop.yml"
- "rubocop/**/*"
- ".rubocop_todo/**/*"

View File

@ -149,6 +149,11 @@ export default {
required: false,
default: false,
},
isImported: {
type: Boolean,
required: false,
default: false,
},
isLocked: {
type: Boolean,
required: false,
@ -551,6 +556,7 @@ export default {
v-if="shouldShowStickyHeader"
:is-confidential="isConfidential"
:is-hidden="isHidden"
:is-imported="isImported"
:is-locked="isLocked"
:issuable-status="issuableStatus"
:issuable-type="issuableType"
@ -570,6 +576,7 @@ export default {
:duplicated-to-issue-url="duplicatedToIssueUrl"
:is-first-contribution="isFirstContribution"
:is-hidden="isHidden"
:is-imported="isImported"
:is-locked="isLocked"
:issuable-state="issuableStatus"
:issuable-type="issuableType"

View File

@ -36,6 +36,10 @@ export default {
type: Boolean,
required: true,
},
isImported: {
type: Boolean,
required: true,
},
isLocked: {
type: Boolean,
required: true,
@ -105,6 +109,7 @@ export default {
:created-at="createdAt"
:is-first-contribution="isFirstContribution"
:is-hidden="isHidden"
:is-imported="isImported"
:issuable-state="issuableState"
:issuable-type="issuableType"
:service-desk-reply-to="serviceDeskReplyTo"

View File

@ -4,6 +4,7 @@ import HiddenBadge from '~/issuable/components/hidden_badge.vue';
import LockedBadge from '~/issuable/components/locked_badge.vue';
import { issuableStatusText, STATUS_CLOSED, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
export default {
WORKSPACE_PROJECT,
@ -14,6 +15,7 @@ export default {
GlIntersectionObserver,
GlLink,
HiddenBadge,
ImportedBadge,
LockedBadge,
},
props: {
@ -27,6 +29,11 @@ export default {
required: false,
default: false,
},
isImported: {
type: Boolean,
required: false,
default: false,
},
isLocked: {
type: Boolean,
required: false,
@ -89,6 +96,8 @@ export default {
/>
<locked-badge v-if="isLocked" :issuable-type="issuableType" />
<hidden-badge v-if="isHidden" :issuable-type="issuableType" />
<imported-badge v-if="isImported" :importable-type="issuableType" />
<gl-link
class="gl-font-weight-bold gl-text-black-normal gl-text-truncate"
href="#top"

View File

@ -1,12 +1,5 @@
<script>
import {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlIcon,
GlLink,
GlSearchBoxByType,
} from '@gitlab/ui';
import { GlButtonGroup, GlCollapsibleListbox, GlLink } from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
@ -14,11 +7,8 @@ export default {
name: 'DeploymentViewButton',
components: {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlIcon,
GlCollapsibleListbox,
GlLink,
GlSearchBoxByType,
ReviewAppLink,
},
directives: {
@ -48,7 +38,14 @@ export default {
return this.deployment.changes && this.deployment.changes.length > 1;
},
filteredChanges() {
return this.deployment?.changes?.filter((change) => change.path.includes(this.searchTerm));
return this.deployment?.changes
?.filter((change) => change.path.includes(this.searchTerm))
.map((change) => ({ value: change.external_url, text: change.path }));
},
},
methods: {
search(searchTerm) {
this.searchTerm = searchTerm;
},
},
};
@ -62,33 +59,24 @@ export default {
size="small"
css-class="deploy-link js-deploy-url gl-display-inline"
/>
<gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
<template #button-content>
<gl-icon
class="dropdown-chevron gl-mx-0!"
name="chevron-down"
data-testid="mr-wigdet-deployment-dropdown-icon"
/>
</template>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="change in filteredChanges"
:key="change.path"
class="js-filtered-dropdown-result"
>
<gl-link
:href="change.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url-menu-item menu-item"
>
<strong class="str-truncated-100 gl-mb-0 gl-display-block">{{ change.path }}</strong>
<p class="text-secondary str-truncated-100 gl-mb-0 d-block">
{{ change.external_url }}
</p>
<gl-collapsible-listbox
:items="filteredChanges"
size="small"
placement="right"
searchable
@search="search"
>
<template #list-item="{ item }">
<gl-link :href="item.value" target="_blank" rel="noopener noreferrer nofollow">
<div>
<strong class="gl-text-truncate gl-mb-0 gl-display-block">{{ item.text }}</strong>
<p class="gl-text-secondary gl-text-truncate gl-mb-0 gl-display-block">
{{ item.value }}
</p>
</div>
</gl-link>
</gl-dropdown-item>
</gl-dropdown>
</template>
</gl-collapsible-listbox>
</gl-button-group>
<review-app-link
v-else

View File

@ -0,0 +1,40 @@
<script>
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
const importableTypeText = {
[TYPE_ISSUE]: __('issue'),
[TYPE_MERGE_REQUEST]: __('merge request'),
};
export default {
components: {
GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
importableType: {
type: String,
required: false,
default: '',
},
},
computed: {
title() {
return sprintf(s__('BulkImport|This %{importable} was imported from another instance.'), {
importable: importableTypeText[this.importableType],
});
},
},
};
</script>
<template>
<gl-badge v-gl-tooltip="title">
{{ __('Imported') }}
</gl-badge>
</template>

View File

@ -7,6 +7,7 @@ import { issuableStatusText, STATUS_OPEN, STATUS_REOPENED } from '~/issues/const
import { isExternal } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@ -20,6 +21,7 @@ export default {
GlSprintf,
HiddenBadge,
LockedBadge,
ImportedBadge,
TimeAgoTooltip,
WorkItemTypeIcon,
},
@ -70,6 +72,11 @@ export default {
required: false,
default: false,
},
isImported: {
type: Boolean,
required: false,
default: false,
},
issuableType: {
type: String,
required: false,
@ -170,6 +177,8 @@ export default {
/>
<locked-badge v-if="blocked" :issuable-type="issuableType" />
<hidden-badge v-if="isHidden" :issuable-type="issuableType" />
<imported-badge v-if="isImported" :importable-type="issuableType" />
<work-item-type-icon
v-if="shouldShowWorkItemTypeIcon"
show-text

View File

@ -1,5 +1,4 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import FormUrlApp from './form_url_app.vue';
import FormCustomHeaders from './form_custom_headers.vue';
@ -8,7 +7,6 @@ export default {
FormUrlApp,
FormCustomHeaders,
},
mixins: [glFeatureFlagsMixin()],
props: {
initialUrl: {
type: String,
@ -32,9 +30,6 @@ export default {
<template>
<div>
<form-url-app :initial-url="initialUrl" :initial-url-variables="initialUrlVariables" />
<form-custom-headers
v-if="glFeatures.customWebhookHeaders"
:initial-custom-headers="initialCustomHeaders"
/>
<form-custom-headers :initial-custom-headers="initialCustomHeaders" />
</div>
</template>

View File

@ -207,6 +207,7 @@ export default {
:multiple="multiSelect"
:searchable="searchable"
start-opened
block
is-check-centered
:infinite-scroll="infiniteScroll"
:searching="loading"
@ -217,7 +218,7 @@ export default {
:selected="localSelectedItem"
:reset-button-label="resetButton"
:infinite-scroll-loading="infiniteScrollLoading"
toggle-class="gl-w-full! work-item-sidebar-dropdown-toggle"
toggle-class="work-item-sidebar-dropdown-toggle"
@reset="unassignValue"
@search="debouncedSearchKeyUpdate"
@select="handleItemClick"

View File

@ -67,7 +67,8 @@ export default {
},
data() {
return {
localAssigneeIds: this.assignees.map(({ id }) => id),
localAssigneeIds: [],
assigneeIdsToShowAtTopOfTheListbox: [],
searchStarted: false,
searchKey: '',
users: [],
@ -111,14 +112,51 @@ export default {
},
},
computed: {
shouldShowParticipants() {
return this.searchKey === '';
},
searchUsers() {
const allUsers = this.shouldShowParticipants
? unionBy(this.users, this.participants, 'id')
: this.users;
return allUsers.map((user) => ({
// when there is no search text, then we show selected users first
// followed by participants, then all other users
if (this.searchKey === '') {
const alphabetizedUsers = unionBy(this.users, this.participants, 'id').sort(
sortNameAlphabetically,
);
if (alphabetizedUsers.length === 0) {
return [];
}
const currentUser = alphabetizedUsers.find(({ id }) => id === this.currentUser?.id);
const allUsers = unionBy([currentUser], alphabetizedUsers, 'id').map((user) => ({
...user,
value: user?.id,
text: user?.name,
}));
const selectedUsers =
allUsers
.filter(({ id }) => this.assigneeIdsToShowAtTopOfTheListbox.includes(id))
.sort(sortNameAlphabetically) || [];
const unselectedUsers = allUsers.filter(
({ id }) => !this.assigneeIdsToShowAtTopOfTheListbox.includes(id),
);
// don't show the selected section if it's empty
if (selectedUsers.length === 0) {
return allUsers.map((user) => ({
...user,
value: user?.id,
text: user?.name,
}));
}
return [
{ options: selectedUsers, text: __('Selected') },
{ options: unselectedUsers, text: __('All users'), textSrOnly: true },
];
}
return this.users.map((user) => ({
...user,
value: user?.id,
text: user?.name,
@ -174,8 +212,15 @@ export default {
assignees: {
handler(newVal) {
this.localAssigneeIds = newVal.map(({ id }) => id);
this.assigneeIdsToShowAtTopOfTheListbox = this.localAssigneeIds;
},
deep: true,
immediate: true,
},
searchKey(newVal, oldVal) {
if (newVal === '' && oldVal !== '') {
this.assigneeIdsToShowAtTopOfTheListbox = this.localAssigneeIds;
}
},
},
methods: {
@ -241,7 +286,8 @@ export default {
this.searchStarted = true;
},
onDropdownHide() {
this.setSearchKey('', false);
this.setSearchKey('');
this.assigneeIdsToShowAtTopOfTheListbox = this.localAssigneeIds;
},
},
};
@ -271,7 +317,7 @@ export default {
@dropdownHidden="onDropdownHide"
>
<template #list-item="{ item }">
<sidebar-participant :user="item" />
<sidebar-participant v-if="item" :user="item" />
</template>
<template v-if="canInviteMembers" #footer>
<gl-button category="tertiary" block class="gl-justify-content-start!">

View File

@ -10,14 +10,6 @@ module WebHooks
before_action :hook_logs, only: :edit
feature_category :webhooks
before_action only: %i[edit update] do
push_frontend_feature_flag(:custom_webhook_headers, hook.parent, type: :beta)
end
before_action only: :index do
push_frontend_feature_flag(:custom_webhook_headers, @project || @group, type: :beta)
end
end
def index

View File

@ -355,7 +355,6 @@ module SearchHelper
end
# Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.order_id_desc.search(term, include_namespace: true, use_minimum_char_limit: false)
.sorted_by_stars_desc.non_archived.limit(limit).map do |p|
@ -391,13 +390,13 @@ module SearchHelper
def recent_merge_requests_autocomplete(term)
return [] unless current_user
::Gitlab::Search::RecentMergeRequests.new(user: current_user).search(term).map do |mr|
::Gitlab::Search::RecentMergeRequests.new(user: current_user).search(term).preload_routables.map do |mr|
{
category: "Recent merge requests",
id: mr.id,
label: search_result_sanitize(mr.title),
url: merge_request_path(mr),
avatar_url: mr.project.avatar_url || '',
avatar_url: mr.target_project.avatar_url || '',
project_id: mr.target_project_id,
project_name: mr.target_project.name
}
@ -407,7 +406,7 @@ module SearchHelper
def recent_issues_autocomplete(term)
return [] unless current_user
::Gitlab::Search::RecentIssues.new(user: current_user).search(term).map do |i|
::Gitlab::Search::RecentIssues.new(user: current_user).search(term).preload_namespace.preload_routables.map do |i|
{
category: "Recent issues",
id: i.id,
@ -419,7 +418,6 @@ module SearchHelper
}
end
end
# rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str)
Sanitize.clean(str)

View File

@ -171,6 +171,9 @@ class Issue < ApplicationRecord
end
scope :preload_awardable, -> { preload(:award_emoji) }
scope :preload_namespace, -> { preload(:namespace) }
scope :preload_routables, -> { preload(project: [:route, { namespace: :route }]) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_api_entity_associations, -> {

View File

@ -680,11 +680,7 @@ class MergeRequest < ApplicationRecord
def committers(with_merge_commits: false, lazy: false)
strong_memoize_with(:committers, with_merge_commits, lazy) do
if Feature.enabled?(:lazy_merge_request_committers, project)
commits.committers(with_merge_commits: with_merge_commits, lazy: lazy)
else
commits.committers(with_merge_commits: with_merge_commits)
end
commits.committers(with_merge_commits: with_merge_commits, lazy: lazy)
end
end

View File

@ -7,7 +7,7 @@ module Clusters
class RefreshService
include Gitlab::Utils::StrongMemoize
AUTHORIZED_ENTITY_LIMIT = 100
AUTHORIZED_ENTITY_LIMIT = 500
delegate :project, to: :agent, private: true
delegate :root_ancestor, to: :project, private: true

View File

@ -7,7 +7,7 @@ module Clusters
class RefreshService
include Gitlab::Utils::StrongMemoize
AUTHORIZED_ENTITY_LIMIT = 100
AUTHORIZED_ENTITY_LIMIT = 500
delegate :project, to: :agent, private: true
delegate :root_ancestor, to: :project, private: true

View File

@ -218,7 +218,6 @@ class WebHookService
def build_custom_headers(values_redacted: false)
return {} unless hook.custom_headers.present?
return {} unless Feature.enabled?(:custom_webhook_headers, hook.parent, type: :beta)
return hook.custom_headers.transform_values { '[REDACTED]' } if values_redacted

View File

@ -1,9 +0,0 @@
---
name: custom_webhook_headers
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/17290
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146702
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/448604
milestone: '16.11'
group: group::import and integrate
type: beta
default_enabled: true

View File

@ -1,9 +0,0 @@
---
name: lazy_merge_request_committers
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441204
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146297
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443883
milestone: '16.10'
group: group::code review
type: gitlab_com_derisk
default_enabled: false

View File

@ -7,4 +7,6 @@ feature_categories:
description: Stores approval policy rules.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146504
milestone: '16.11'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
sharding_key:
security_policy_management_project_id: projects

View File

@ -7,4 +7,6 @@ feature_categories:
description: Stores policy data.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146504
milestone: '16.11'
gitlab_schema: gitlab_main
gitlab_schema: gitlab_main_cell
sharding_key:
security_policy_management_project_id: projects

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSecurityPolicyManagementProjectIdToSecurityPolicies < Gitlab::Database::Migration[2.2]
milestone '17.0'
def up
# rubocop:disable Rails/NotNullColumn -- table is empty
add_column :security_policies, :security_policy_management_project_id, :bigint, null: false
# rubocop:enable Rails/NotNullColumn
end
def down
remove_column :security_policies, :security_policy_management_project_id
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddSecurityPolicyManagementProjectIdFkToSecurityPolicies < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :security_policies,
:projects,
column: :security_policy_management_project_id,
on_delete: :cascade
end
def down
remove_foreign_key_if_exists :security_policies, column: :security_policy_management_project_id
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddIndexSecurityPolicyManagementProjectIdOnSecurityPolicies < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
INDEX_NAME = 'index_security_policies_on_policy_management_project_id'
def up
add_concurrent_index :security_policies, :security_policy_management_project_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :security_policies, INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddSecurityPolicyManagementProjectIdToApprovalPolicyRules < Gitlab::Database::Migration[2.2]
milestone '17.0'
def up
# rubocop:disable Rails/NotNullColumn -- table is empty
add_column :approval_policy_rules, :security_policy_management_project_id, :bigint, null: false
# rubocop:enable Rails/NotNullColumn
end
def down
remove_column :approval_policy_rules, :security_policy_management_project_id
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddSecurityPolicyManagementProjectIdFkToApprovalPolicyRules < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :approval_policy_rules,
:projects,
column: :security_policy_management_project_id,
on_delete: :cascade
end
def down
remove_foreign_key_if_exists :approval_policy_rules, column: :security_policy_management_project_id
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddIndexSecurityPolicyManagementProjectIdOnApprovalPolicyRules < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
INDEX_NAME = 'index_approval_policy_rules_on_policy_management_project_id'
def up
add_concurrent_index :approval_policy_rules, :security_policy_management_project_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :approval_policy_rules, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
5961e034705392a82a709fccddc32d2cbbde016d0c0b1db3f66af1be0573928b

View File

@ -0,0 +1 @@
17716005af88da4cb7905faeba87e31462f50f61eef25f671b338c4cb01025d6

View File

@ -0,0 +1 @@
f5c9b34ef88af798bc6f6918d9a04aef29377d9c9f4d07a488f890cb8d1527c2

View File

@ -0,0 +1 @@
7eacbbbe4f7e0e0fc2cc15369558126d03e6a8dd58eafc7281eb79fd4d0aa80f

View File

@ -0,0 +1 @@
295e692b5ada0d84cf4ba1b64f6e56237e2d09e1be3cb0674b61ab238884bb2c

View File

@ -0,0 +1 @@
50b095186f8ebaa709f7918307b77224ea35ebb56bd9c8e3fe5031ed977af1be

View File

@ -4602,7 +4602,8 @@ CREATE TABLE approval_policy_rules (
updated_at timestamp with time zone NOT NULL,
rule_index smallint NOT NULL,
type smallint NOT NULL,
content jsonb DEFAULT '{}'::jsonb NOT NULL
content jsonb DEFAULT '{}'::jsonb NOT NULL,
security_policy_management_project_id bigint NOT NULL
);
CREATE SEQUENCE approval_policy_rules_id_seq
@ -15795,6 +15796,7 @@ CREATE TABLE security_policies (
scope jsonb DEFAULT '{}'::jsonb NOT NULL,
actions jsonb DEFAULT '[]'::jsonb NOT NULL,
approval_settings jsonb DEFAULT '{}'::jsonb NOT NULL,
security_policy_management_project_id bigint NOT NULL,
CONSTRAINT check_3fa0f29e4b CHECK ((char_length(name) <= 255)),
CONSTRAINT check_966e08b242 CHECK ((char_length(checksum) <= 255)),
CONSTRAINT check_99c8e08928 CHECK ((char_length(description) <= 255))
@ -24323,6 +24325,8 @@ CREATE UNIQUE INDEX index_approval_merge_request_rules_users_1 ON approval_merge
CREATE INDEX index_approval_merge_request_rules_users_2 ON approval_merge_request_rules_users USING btree (user_id);
CREATE INDEX index_approval_policy_rules_on_policy_management_project_id ON approval_policy_rules USING btree (security_policy_management_project_id);
CREATE UNIQUE INDEX index_approval_policy_rules_on_unique_policy_rule_index ON approval_policy_rules USING btree (security_policy_id, rule_index);
CREATE UNIQUE INDEX index_approval_project_rules_groups_1 ON approval_project_rules_groups USING btree (approval_project_rule_id, group_id);
@ -27151,6 +27155,8 @@ CREATE INDEX p_ci_builds_name_id_idx ON ONLY p_ci_builds USING btree (name, id)
CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features ON ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text, ('apifuzzer_fuzz'::character varying)::text, ('apifuzzer_fuzz_dnd'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
CREATE INDEX index_security_policies_on_policy_management_project_id ON security_policies USING btree (security_policy_management_project_id);
CREATE UNIQUE INDEX index_security_policies_on_unique_config_type_policy_index ON security_policies USING btree (security_orchestration_policy_configuration_id, type, policy_index);
CREATE INDEX index_security_scans_for_non_purged_records ON security_scans USING btree (created_at, id) WHERE (status <> 6);
@ -29786,6 +29792,9 @@ ALTER TABLE ONLY merge_requests
ALTER TABLE ONLY sbom_occurrences_vulnerabilities
ADD CONSTRAINT fk_07b81e3a81 FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY security_policies
ADD CONSTRAINT fk_08722e8ac7 FOREIGN KEY (security_policy_management_project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY abuse_report_user_mentions
ADD CONSTRAINT fk_088018ecd8 FOREIGN KEY (abuse_report_id) REFERENCES abuse_reports(id) ON DELETE CASCADE;
@ -30809,6 +30818,9 @@ ALTER TABLE p_ci_builds_metadata
ALTER TABLE ONLY gitlab_subscriptions
ADD CONSTRAINT fk_e2595d00a1 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY approval_policy_rules
ADD CONSTRAINT fk_e344cb2d35 FOREIGN KEY (security_policy_management_project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY abuse_events
ADD CONSTRAINT fk_e5ce49c215 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -91,6 +91,7 @@ Outputs can be the following types:
- `struct`
- `raw_string`
- `step_result`
Outputs are written to `${{ output_file }}` in the form `key=value` where `key` is the name of the output.
The `value` should be written as JSON unless the type is `raw_string`.
The value type written by the step must match the declared type. The default output type is `raw_string`.

View File

@ -18,6 +18,7 @@ DETAILS:
> - Support for Linux package installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5.
> - The ability to switch between certificate-based clusters and agents was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335089) in GitLab 14.9. The certificate-based cluster context is always called `gitlab-deploy`.
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80508) from _CI/CD tunnel_ to _CI/CD workflow_ in GitLab 14.9.
> - The [limit of agent connection sharing was raised](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149844) from 100 to 500 in GitLab 17.0
You can use GitLab CI/CD to safely connect, deploy, and update your Kubernetes clusters.
@ -80,7 +81,7 @@ To authorize the agent to access the GitLab project where you keep Kubernetes ma
- Authorized projects must have the same root group or user namespace as the agent's configuration project.
- You can install additional agents into the same cluster to accommodate additional hierarchies.
- You can authorize up to 100 projects.
- You can authorize up to 500 projects.
All CI/CD jobs now include a `kubeconfig` file with contexts for every shared agent connection.
The `kubeconfig` path is available in the environment variable `$KUBECONFIG`.
@ -106,7 +107,7 @@ To authorize the agent to access all of the GitLab projects in a group or subgro
- Authorized groups must have the same root group as the agent's configuration project.
- You can install additional agents into the same cluster to accommodate additional hierarchies.
- All of the subgroups of an authorized group also have access to the same agent (without being specified individually).
- You can authorize up to 100 groups.
- You can authorize up to 500 groups.
All the projects that belong to the group and its subgroups are now authorized to access the agent.
All CI/CD jobs now include a `kubeconfig` file with contexts for every shared agent connection.

View File

@ -14,6 +14,7 @@ DETAILS:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390769) in GitLab 16.1, with [flags](../../../administration/feature_flags.md) named `environment_settings_to_graphql`, `kas_user_access`, `kas_user_access_project`, and `expose_authorized_cluster_agents`. This feature is in [Beta](../../../policy/experiment-beta-support.md#beta).
> - Feature flag `environment_settings_to_graphql` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124177) in GitLab 16.2.
> - Feature flags `kas_user_access`, `kas_user_access_project`, and `expose_authorized_cluster_agents` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125835) in GitLab 16.2.
> - The [limit of agent connection sharing was raised](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149844) from 100 to 500 in GitLab 17.0
As an administrator of Kubernetes clusters in an organization, you can grant Kubernetes access to members
of a specific project or group.
@ -39,8 +40,8 @@ To configure access:
- In the agent configuration file, define a `user_access` keyword with the following parameters:
- `projects`: A list of projects whose members should have access.
- `groups`: A list of groups whose members should have access.
- `projects`: A list of projects whose members should have access. You can authorize up to 500 projects.
- `groups`: A list of groups whose members should have access. You can authorize up to 500 projects.
- `access_as`: Required. For plain access, the value is `{ agent: {...} }`.
After you configure access, requests are forwarded to the API server using

View File

@ -110,11 +110,7 @@ Otherwise, a `URI is invalid` error occurs.
## Custom headers
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146702) in GitLab 16.11 [with a flag](../../../administration/feature_flags.md) named `custom_webhook_headers`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can
[disable the feature flag](../../../administration/feature_flags.md) named `custom_webhook_headers`.
On GitLab.com and GitLab Dedicated, this feature is available.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/448604) in GitLab 17.0. Feature flag `custom_webhook_headers` removed.
You can add up to 20 custom headers in the webhook configuration as part of the request.
You can use these custom headers for authentication to external services.

View File

@ -61,7 +61,7 @@ opentofu:use-component-instead-of-template:
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
when: never
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
#allow_failure: true
# allow_failure: true
allow_failure: true
cache:
key: "$TF_ROOT"

View File

@ -26,7 +26,7 @@ variables:
before_script:
- apk add --no-cache go curl bash nodejs
## Uncomment the following if you use PostCSS. See https://gohugo.io/hugo-pipes/postcss/
#- npm install postcss postcss-cli autoprefixer
# - npm install postcss postcss-cli autoprefixer
test:
script:

View File

@ -35,7 +35,7 @@ module Gitlab
def search(term)
finder.new(user, search: term, in: 'title', skip_full_text_search_project_condition: true)
.execute
.limit(SEARCH_LIMIT).reorder(nil).id_in_ordered(latest_ids) # rubocop: disable CodeReuse/ActiveRecord
.limit(SEARCH_LIMIT).without_order.id_in_ordered(latest_ids)
end
private

View File

@ -5104,6 +5104,9 @@ msgstr ""
msgid "All threads resolved!"
msgstr ""
msgid "All users"
msgstr ""
msgid "All users in this group must set up two-factor authentication"
msgstr ""
@ -9616,6 +9619,9 @@ msgstr ""
msgid "BulkImport|Template / File-based import / Direct transfer"
msgstr ""
msgid "BulkImport|This %{importable} was imported from another instance."
msgstr ""
msgid "BulkImport|Unsupported GitLab version. Minimum supported version is '%{version}'."
msgstr ""
@ -26577,6 +26583,9 @@ msgstr ""
msgid "ImportProjects|Update of imported projects with realtime changes failed"
msgstr ""
msgid "Imported"
msgstr ""
msgid "Imported relation must be one of %{relations}"
msgstr ""

View File

@ -63,19 +63,6 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
end
context 'when CI is running' do
let(:status) { :running }
it 'does not allow to merge immediately' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page).to have_button 'Set to auto-merge'
expect(page).not_to have_button '.js-merge-moment'
end
end
context 'when CI failed' do
let(:status) { :failed }
@ -131,6 +118,76 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
expect(page).not_to have_button('Merge', exact: true)
end
end
context 'when CI is running', :sidekiq_inline do
let(:status) { :running }
it 'does not allow to merge immediately' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page).to have_button 'Set to auto-merge'
expect(page).not_to have_button '.js-merge-moment'
end
context 'when auto-merge is set' do
before do
visit project_merge_request_path(project, merge_request)
wait_for_requests
click_button('Set to auto-merge')
wait_for_requests
end
context 'when CI passes' do
before do
pipeline.set_status('success')
end
it 'the MR gets merged' do
expect(page).to have_content("Pipeline ##{pipeline.id} passed")
wait_for_requests
expect(page).to have_content('Merged by')
end
end
context 'when CI fails' do
before do
pipeline.set_status('failed')
end
it 'MR is blocked' do
expect(page).to have_content("Pipeline ##{pipeline.id} failed")
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_content('Merge blocked')
end
end
end
context 'when CI is canceled' do
before do
pipeline.set_status('canceled')
end
it 'MR is blocked' do
expect(page).to have_content("Pipeline ##{pipeline.id} canceled")
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_content('Merge blocked')
end
end
end
end
end
end
context 'when merge requests can be merged when the build failed' do
@ -138,7 +195,21 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, false)
end
context 'when CI is running' do
context 'when CI failed' do
let(:status) { :failed }
it 'allows MR to be merged' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_button 'Merge'
end
end
end
context 'when CI is running', :sidekiq_inline do
let(:status) { :running }
it 'allows MR to be merged immediately' do
@ -151,17 +222,61 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
page.find('.js-merge-moment').click
expect(page).to have_content 'Merge immediately'
end
end
context 'when CI failed' do
let(:status) { :failed }
context 'when auto-merge is set' do
before do
visit project_merge_request_path(project, merge_request)
it 'allows MR to be merged' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_button 'Merge'
click_button('Set to auto-merge')
wait_for_requests
end
context 'when CI passes' do
before do
pipeline.set_status('success')
end
it 'the MR gets merged' do
expect(page).to have_content("Pipeline ##{pipeline.id} passed")
wait_for_requests
expect(page).to have_content('Merged by')
end
end
context 'when CI fails' do
before do
pipeline.set_status('failed')
end
it 'MR remains set to auto-merge' do
expect(page).to have_content("Pipeline ##{pipeline.id} failed")
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_content('Ready to merge')
end
end
end
context 'when CI is canceled' do
before do
pipeline.set_status('canceled')
end
it 'MR remains set to auto-merge' do
expect(page).to have_content("Pipeline ##{pipeline.id} canceled")
wait_for_requests
page.within('.mr-state-widget') do
expect(page).to have_content('Ready to merge')
end
end
end
end
end

View File

@ -20,6 +20,7 @@ describe('IssueHeader component', () => {
duplicatedToIssueUrl: '',
isFirstContribution: false,
isHidden: false,
isImported: false,
isLocked: false,
issuableState: 'opened',
issuableType: 'issue',
@ -43,6 +44,7 @@ describe('IssueHeader component', () => {
createdAt: '2020-01-23T12:34:56.789Z',
isFirstContribution: false,
isHidden: false,
isImported: false,
issuableState: 'opened',
issuableType: 'issue',
serviceDeskReplyTo: '',

View File

@ -13,12 +13,14 @@ import {
} from '~/issues/constants';
import StickyHeader from '~/issues/show/components/sticky_header.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
describe('StickyHeader component', () => {
let wrapper;
const findConfidentialBadge = () => wrapper.findComponent(ConfidentialityBadge);
const findHiddenBadge = () => wrapper.findComponent(HiddenBadge);
const findImportedBadge = () => wrapper.findComponent(ImportedBadge);
const findLockedBadge = () => wrapper.findComponent(LockedBadge);
const findTitle = () => wrapper.findComponent(GlLink);
@ -102,6 +104,16 @@ describe('StickyHeader component', () => {
expect(hiddenBadge.exists()).toBe(isHidden);
});
it.each`
title | isImported
${'does not show imported badge when issue is not imported'} | ${false}
${'shows imported badge when issue is imported'} | ${true}
`('$title', ({ isImported }) => {
createComponent({ isImported });
expect(findImportedBadge().exists()).toBe(isImported);
});
it('shows with title', () => {
createComponent();
const title = findTitle();

View File

@ -1,4 +1,4 @@
import { GlDropdown, GlLink } from '@gitlab/ui';
import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
@ -28,9 +28,7 @@ describe('Deployment View App button', () => {
});
const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink);
const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown);
const findMrWigdetDeploymentDropdownIcon = () =>
wrapper.findByTestId('mr-wigdet-deployment-dropdown-icon');
const findMrWidgetDeploymentDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink);
describe('text', () => {
@ -50,7 +48,7 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app without dropdown', () => {
expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
expect(findMrWidgetDeploymentDropdown().exists()).toBe(false);
});
});
@ -65,8 +63,7 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app without dropdown', () => {
expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
expect(findMrWidgetDeploymentDropdown().exists()).toBe(false);
});
it('renders the link to the review app linked to to the first change', () => {
@ -87,8 +84,15 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app with dropdown', () => {
expect(findMrWigdetDeploymentDropdown().exists()).toBe(true);
expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(true);
const dropdown = findMrWidgetDeploymentDropdown();
const thirdChangeUrl = deploymentMockData.changes[2].external_url;
expect(dropdown.exists()).toBe(true);
const links = dropdown.findAll('a');
expect(links.length).toBe(3);
expect(links.at(2).attributes('href')).toBe(thirdChangeUrl);
});
it('renders all the links to the review apps', () => {

View File

@ -0,0 +1,49 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
describe('ImportedBadge', () => {
let wrapper;
const defaultProps = {
importableType: TYPE_ISSUE,
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(ImportedBadge, {
propsData: {
...defaultProps,
...props,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const findBadge = () => wrapper.findComponent(GlBadge);
const findBadgeTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
it('renders "Imported" badge', () => {
createComponent();
expect(findBadge().text()).toBe('Imported');
});
it.each`
importableType | tooltipText
${TYPE_ISSUE} | ${'This issue was imported from another instance.'}
${TYPE_MERGE_REQUEST} | ${'This merge request was imported from another instance.'}
`('renders tooltip for $importableType', ({ importableType, tooltipText }) => {
createComponent({
props: {
importableType,
},
});
expect(findBadgeTooltip().value).toBe(tooltipText);
});
});

View File

@ -7,6 +7,7 @@ import LockedBadge from '~/issuable/components/locked_badge.vue';
import { STATUS_CLOSED, STATUS_OPEN, STATUS_REOPENED, TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@ -27,6 +28,7 @@ describe('IssuableHeader component', () => {
findGlIconWithName(name).exists() ? findGlIconWithName(name).at(0) : undefined;
const findBlockedBadge = () => wrapper.findComponent(LockedBadge);
const findHiddenBadge = () => wrapper.findComponent(HiddenBadge);
const findImportedBadge = () => wrapper.findComponent(ImportedBadge);
const findExternalLinkIcon = () => findIcon('external-link');
const findFirstContributionIcon = () => findIcon('first-contribution');
const findComponentTooltip = (component) => getBinding(component.element, 'gl-tooltip');
@ -141,6 +143,20 @@ describe('IssuableHeader component', () => {
});
});
describe('imported badge', () => {
it('renders when issuable is imported', () => {
createComponent({ isImported: true });
expect(findImportedBadge().props('importableType')).toBe('issue');
});
it('does not render when issuable is not imported', () => {
createComponent({ isImported: false });
expect(findImportedBadge().exists()).toBe(false);
});
});
describe('work item type icon', () => {
it('renders when showWorkItemTypeIcon=true and work item type exists', () => {
createComponent({ showWorkItemTypeIcon: true, issuableType: 'issue' });

View File

@ -1,5 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash';
import { sortNameAlphabetically } from '~/work_items/utils';
import WorkItemAssignees from '~/work_items/components/work_item_assignees_with_edit.vue';
import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
@ -57,6 +58,10 @@ describe('WorkItemAssigneesWithEdit component', () => {
findSidebarDropdownWidget().vm.$emit('dropdownShown');
};
const hideDropdown = () => {
findSidebarDropdownWidget().vm.$emit('dropdownHidden');
};
const createComponent = ({
mountFn = shallowMountExtended,
assignees = mockAssignees,
@ -102,7 +107,7 @@ describe('WorkItemAssigneesWithEdit component', () => {
showDropdown();
await waitForPromises();
expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(0);
expect(findSidebarDropdownWidget().props('listItems')).toEqual([]);
});
it('emits error event if search users query fails', async () => {
@ -113,7 +118,7 @@ describe('WorkItemAssigneesWithEdit component', () => {
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
});
it('passes the correct props to clear search text on item select', () => {
it('clears search text on item select', () => {
createComponent();
expect(findSidebarDropdownWidget().props('clearSearchOnItemSelect')).toBe(true);
@ -253,7 +258,7 @@ describe('WorkItemAssigneesWithEdit component', () => {
});
describe('sorting', () => {
it('always sorts assignees based on alphabetical order on the frontend', async () => {
it('sorts assignees based on alphabetical order on the frontend', async () => {
createComponent({ mountFn: mountExtended });
await waitForPromises();
@ -263,6 +268,74 @@ describe('WorkItemAssigneesWithEdit component', () => {
mockAssignees.sort(sortNameAlphabetically),
);
});
it('sorts selected assignees first', async () => {
const [
unselected,
selected,
] = projectMembersAutocompleteResponseWithCurrentUser.data.workspace.users;
createComponent({
assignees: [selected],
});
showDropdown();
await waitForPromises();
expect(findSidebarDropdownWidget().props('listItems')).toMatchObject(
cloneDeep([
{ options: [selected], text: 'Selected' },
{ options: [unselected], text: 'All users', textSrOnly: true },
]),
);
});
it('shows current user above other users', async () => {
const [unselected, currentUser] = cloneDeep(
projectMembersAutocompleteResponseWithCurrentUser.data.workspace.users,
);
createComponent({
assignees: [],
});
showDropdown();
await waitForPromises();
findSidebarDropdownWidget().vm.$emit('updateValue', currentUser.id);
expect(findSidebarDropdownWidget().props('listItems')).toMatchObject([
{ text: currentUser.name },
{ text: unselected.name },
]);
});
it('does not move newly selected assignees to the top until dropdown is closed', async () => {
const [unselected, currentUser] = cloneDeep(
projectMembersAutocompleteResponseWithCurrentUser.data.workspace.users,
);
createComponent({
assignees: [],
});
showDropdown();
await waitForPromises();
findSidebarDropdownWidget().vm.$emit('updateValue', currentUser.id);
expect(findSidebarDropdownWidget().props('listItems')).toMatchObject([
{ text: currentUser.name },
{ text: unselected.name },
]);
hideDropdown();
await waitForPromises();
showDropdown();
await waitForPromises();
expect(findSidebarDropdownWidget().props('listItems')).toMatchObject([
{ options: [currentUser], text: 'Selected' },
{ options: [unselected], text: 'All users', textSrOnly: true },
]);
});
});
describe('invite members', () => {

View File

@ -23,7 +23,7 @@ RSpec.describe SearchHelper, feature_category: :global_search do
end
context "with a standard user" do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
allow(self).to receive(:current_user).and_return(user)
@ -169,106 +169,151 @@ RSpec.describe SearchHelper, feature_category: :global_search do
expect(result.keys).to match_array(%i[category id value label url avatar_url])
end
it 'includes the users recently viewed issues and project with correct order', :aggregate_failures do
recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, :with_avatar, namespace: user.namespace)
project2 = create(:project, namespace: user.namespace)
issue1 = create(:issue, title: 'issue 1', project: project1)
issue2 = create(:issue, title: 'issue 2', project: project2)
project = create(:project, title: 'the search term')
project.add_developer(user)
context 'for recently reviewed items' do
let(:search_term) { 'the search term' }
let(:recent_issues) { instance_double(::Gitlab::Search::RecentIssues) }
let(:recent_merge_requests) { instance_double(::Gitlab::Search::RecentMergeRequests) }
expect(recent_issues).to receive(:search).with('the search term').and_return(Issue.id_in_ordered([issue1.id, issue2.id]))
let_it_be(:project1) { create(:project, namespace: user.namespace) }
let_it_be(:project2) { create(:project) }
results = search_autocomplete_opts("the search term")
it 'includes the users recently viewed issues and project with correct order', :aggregate_failures do
project = create(:project, :with_avatar, title: 'the search term')
project.add_developer(user)
expect(results.count).to eq(3)
issue1 = create(:issue, title: 'issue 1', project: project)
issue2 = create(:issue, title: 'issue 2', project: project2)
expect(results[0]).to include({
category: 'Recent issues',
id: issue1.id,
label: 'issue 1',
url: Gitlab::Routing.url_helpers.project_issue_path(issue1.project, issue1),
avatar_url: project1.avatar_url
})
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
expect(recent_issues).to receive(:search).with(search_term).and_return(Issue.id_in_ordered([issue1.id, issue2.id]))
expect(results[1]).to include({
category: 'Recent issues',
id: issue2.id,
label: 'issue 2',
url: Gitlab::Routing.url_helpers.project_issue_path(issue2.project, issue2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
results = search_autocomplete_opts(search_term)
expect(results[2]).to include({
category: 'Projects',
id: project.id,
label: project.full_name,
url: Gitlab::Routing.url_helpers.project_path(project)
})
end
expect(results.count).to eq(3)
it 'includes the users recently viewed issues with the exact same name', :aggregate_failures do
recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, namespace: user.namespace)
project2 = create(:project, namespace: user.namespace)
issue1 = create(:issue, title: 'issue same_name', project: project1)
issue2 = create(:issue, title: 'issue same_name', project: project2)
expect(results[0]).to include({
category: 'Recent issues',
id: issue1.id,
label: 'issue 1',
url: Gitlab::Routing.url_helpers.project_issue_path(issue1.project, issue1),
avatar_url: project.avatar_url
})
expect(recent_issues).to receive(:search).with('the search term').and_return(Issue.id_in_ordered([issue1.id, issue2.id]))
expect(results[1]).to include({
category: 'Recent issues',
id: issue2.id,
label: 'issue 2',
url: Gitlab::Routing.url_helpers.project_issue_path(issue2.project, issue2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
results = search_autocomplete_opts("the search term")
expect(results[2]).to include({
category: 'Projects',
id: project.id,
label: project.full_name,
url: Gitlab::Routing.url_helpers.project_path(project)
})
end
expect(results.count).to eq(2)
it 'includes the users recently viewed issues with the exact same name', :aggregate_failures do
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project3 = create(:project, :with_avatar, namespace: user.namespace)
issue1 = create(:issue, title: 'issue same_name', project: project3)
issue2 = create(:issue, title: 'issue same_name', project: project2)
expect(results[0]).to include({
category: 'Recent issues',
id: issue1.id,
label: 'issue same_name',
url: Gitlab::Routing.url_helpers.project_issue_path(issue1.project, issue1),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
expect(recent_issues).to receive(:search).with(search_term)
.and_return(Issue.id_in_ordered([issue1.id, issue2.id]))
expect(results[1]).to include({
category: 'Recent issues',
id: issue2.id,
label: 'issue same_name',
url: Gitlab::Routing.url_helpers.project_issue_path(issue2.project, issue2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
end
results = search_autocomplete_opts(search_term)
it 'includes the users recently viewed merge requests', :aggregate_failures do
recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests)
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests)
project1 = create(:project, :with_avatar, namespace: user.namespace)
project2 = create(:project, namespace: user.namespace)
merge_request1 = create(:merge_request, :unique_branches, title: 'Merge request 1', target_project: project1, source_project: project1)
merge_request2 = create(:merge_request, :unique_branches, title: 'Merge request 2', target_project: project2, source_project: project2)
expect(results.count).to eq(2)
expect(recent_merge_requests).to receive(:search).with('the search term').and_return(MergeRequest.id_in_ordered([merge_request1.id, merge_request2.id]))
expect(results[0]).to include({
category: 'Recent issues',
id: issue1.id,
label: 'issue same_name',
url: Gitlab::Routing.url_helpers.project_issue_path(issue1.project, issue1),
avatar_url: project3.avatar_url
})
results = search_autocomplete_opts("the search term")
expect(results[1]).to include({
category: 'Recent issues',
id: issue2.id,
label: 'issue same_name',
url: Gitlab::Routing.url_helpers.project_issue_path(issue2.project, issue2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
end
expect(results.count).to eq(2)
it 'includes the users recently viewed merge requests', :aggregate_failures do
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user)
.and_return(recent_merge_requests)
expect(results[0]).to include({
category: 'Recent merge requests',
id: merge_request1.id,
label: 'Merge request 1',
url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request1.project, merge_request1),
avatar_url: project1.avatar_url
})
merge_request1 = create(:merge_request, :unique_branches,
title: 'Merge request 1', target_project: project1, source_project: project1)
merge_request2 = create(:merge_request, :unique_branches,
title: 'Merge request 2', target_project: project2, source_project: project2)
expect(results[1]).to include({
category: 'Recent merge requests',
id: merge_request2.id,
label: 'Merge request 2',
url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request2.project, merge_request2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
expect(recent_merge_requests).to receive(:search).with(search_term)
.and_return(MergeRequest.id_in_ordered([merge_request1.id, merge_request2.id]))
results = search_autocomplete_opts(search_term)
expect(results.count).to eq(2)
expect(results[0]).to include({
category: 'Recent merge requests',
id: merge_request1.id,
label: 'Merge request 1',
url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request1.project, merge_request1),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
expect(results[1]).to include({
category: 'Recent merge requests',
id: merge_request2.id,
label: 'Merge request 2',
url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request2.project, merge_request2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
end
it 'does not have an N+1 for recently viewed issues' do
issue1 = create(:issue, title: 'issue 1', project: project1)
issue2 = create(:issue, title: 'issue 2', project: project2)
issue_ids = [issue1.id, issue2.id]
allow(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
expect(recent_issues).to receive(:search).with(search_term).and_return(Issue.id_in_ordered(issue_ids))
control = ActiveRecord::QueryRecorder.new(skip_cached: true) { search_autocomplete_opts(search_term) }
issue_ids += create_list(:issue, 3).map(&:id)
expect(recent_issues).to receive(:search).with(search_term).and_return(Issue.id_in_ordered(issue_ids))
expect { search_autocomplete_opts(search_term) }.to issue_same_number_of_queries_as(control)
end
it 'does not have an N+1 for recently viewed merge_requests' do
merge_request1 = create(:merge_request, :unique_branches,
title: 'Merge request 1', target_project: project1, source_project: project1)
merge_request2 = create(:merge_request, :unique_branches,
title: 'Merge request 2', target_project: project2, source_project: project2)
merge_request_ids = [merge_request1.id, merge_request2.id]
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user)
.and_return(recent_merge_requests).twice
expect(recent_merge_requests).to receive(:search).with(search_term)
.and_return(MergeRequest.id_in_ordered(merge_request_ids))
control = ActiveRecord::QueryRecorder.new(skip_cached: true) { search_autocomplete_opts(search_term) }
merge_request_ids += create_list(:merge_request, 3, :unique_branches).map(&:id)
expect(recent_merge_requests).to receive(:search).with(search_term)
.and_return(MergeRequest.id_in_ordered(merge_request_ids))
expect { search_autocomplete_opts(search_term) }.to issue_same_number_of_queries_as(control)
end
end
it "does not include the public group" do
@ -435,12 +480,15 @@ RSpec.describe SearchHelper, feature_category: :global_search do
let_it_be(:project) { create(:project, name: 'Searched') }
let_it_be(:issue) { create(:issue, title: 'Searched', project: project) }
before_all do
project.add_developer(user)
end
before do
allow(self).to receive(:current_user).and_return(user)
allow_next_instance_of(Gitlab::Search::RecentIssues) do |recent_issues|
allow(recent_issues).to receive(:search).and_return([issue])
allow(recent_issues).to receive(:search).and_return(Issue.id_in(issue.id))
end
project.add_developer(user)
end
where(:scope, :category) do

View File

@ -12,8 +12,6 @@ RSpec.describe 'new tables with gitlab_main schema', feature_category: :cell do
# Specific tables can be exempted from this requirement, and such tables must be added to the `exempted_tables` list.
let!(:exempted_tables) do
[
"approval_policy_rules", # https://gitlab.com/gitlab-org/gitlab/-/issues/452380
"security_policies", # https://gitlab.com/gitlab-org/gitlab/-/issues/452380
"audit_events_instance_amazon_s3_configurations", # https://gitlab.com/gitlab-org/gitlab/-/issues/431327
"sbom_source_packages" # https://gitlab.com/gitlab-org/gitlab/-/issues/437718
]

View File

@ -340,6 +340,27 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
describe 'scopes for preloading' do
before_all do
create(:issue, project: reusable_project)
end
describe '.preload_namespace' do
subject(:preload_namespace) { described_class.in_projects(reusable_project).preload_namespace }
it { expect(preload_namespace.first.association(:namespace)).to be_loaded }
end
describe '.preload_routables' do
subject(:preload_routables) { described_class.in_projects(reusable_project).preload_routables }
it { expect(preload_routables.first.association(:project)).to be_loaded }
it { expect(preload_routables.first.project.association(:route)).to be_loaded }
it { expect(preload_routables.first.project.association(:namespace)).to be_loaded }
it { expect(preload_routables.first.project.namespace.association(:route)).to be_loaded }
end
end
context 'order by upvotes' do
let!(:issue) { create(:issue) }
let!(:issue2) { create(:issue) }

View File

@ -2145,50 +2145,6 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(subject.committers(lazy: true)).to eq(committers)
end
end
context 'when lazy_merge_request_committers feature flag is disabled' do
before do
stub_feature_flags(lazy_merge_request_committers: false)
end
context 'when not given with_merge_commits and lazy' do
it 'calls committers on the commits object with the expected param' do
expect(subject).to receive(:commits).and_return(commits)
expect(commits).to receive(:committers).with(with_merge_commits: false).and_return(committers)
expect(subject.committers).to eq(committers)
end
context 'when with_merge_commits and lazy arguments changes' do
it 'does not use memoized value' do
subject.committers # this memoizes the value with with_merge_commits and lazy as false
expect(subject).to receive(:commits).and_return(commits)
expect(commits).to receive(:committers).with(with_merge_commits: true).and_return(committers)
subject.committers(with_merge_commits: true, lazy: true)
end
end
end
context 'when given with_merge_commits true' do
it 'calls committers on the commits object with the expected param' do
expect(subject).to receive(:commits).and_return(commits)
expect(commits).to receive(:committers).with(with_merge_commits: true).and_return(committers)
expect(subject.committers(with_merge_commits: true)).to eq(committers)
end
end
context 'when given lazy true' do
it 'calls committers on the commits object with the expected param' do
expect(subject).to receive(:commits).and_return(commits)
expect(commits).to receive(:committers).with(with_merge_commits: false).and_return(committers)
expect(subject.committers(lazy: true)).to eq(committers)
end
end
end
end
describe '#diverged_commits_count' do

View File

@ -464,19 +464,6 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
.with(headers: Gitlab::WebHooks::RecursionDetection.header(project_hook))
end
end
context 'when custom_webhook_headers feature flag is disabled' do
before do
stub_feature_flags(custom_webhook_headers: false)
end
it 'sends request without custom headers' do
service_instance.execute
expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url))
.with(headers: headers)
end
end
end
it 'handles 200 status code' do