Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
98decb267a
commit
92d681f6f0
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/**/*"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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!">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
5961e034705392a82a709fccddc32d2cbbde016d0c0b1db3f66af1be0573928b
|
||||
|
|
@ -0,0 +1 @@
|
|||
17716005af88da4cb7905faeba87e31462f50f61eef25f671b338c4cb01025d6
|
||||
|
|
@ -0,0 +1 @@
|
|||
f5c9b34ef88af798bc6f6918d9a04aef29377d9c9f4d07a488f890cb8d1527c2
|
||||
|
|
@ -0,0 +1 @@
|
|||
7eacbbbe4f7e0e0fc2cc15369558126d03e6a8dd58eafc7281eb79fd4d0aa80f
|
||||
|
|
@ -0,0 +1 @@
|
|||
295e692b5ada0d84cf4ba1b64f6e56237e2d09e1be3cb0674b61ab238884bb2c
|
||||
|
|
@ -0,0 +1 @@
|
|||
50b095186f8ebaa709f7918307b77224ea35ebb56bd9c8e3fe5031ed977af1be
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue