Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-06 12:07:17 +00:00
parent dcaa8f80fb
commit 08c811d7ce
106 changed files with 2116 additions and 2159 deletions

View File

@ -330,6 +330,9 @@ That's all of the required database changes.
```
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#repository` method which should return a `<Repository>` instance, and implement the class method `.model` to return the `CoolWidget` class:
- Implement the `replicable_title` and `replicable_title_plural` methods to
return the human-readable singular and pluralized title of the replicable,
which will be displayed in the UI and Rails console
```ruby
# frozen_string_literal: true
@ -343,6 +346,16 @@ That's all of the required database changes.
::CoolWidget
end
# @return [String] human-readable title.
def self.replicable_title
s_('Geo|Cool Widget')
end
# @return [String] pluralized human-readable title.
def self.replicable_title_plural
s_('Geo|Cool Widgets')
end
override :verification_feature_flag_enabled?
def self.verification_feature_flag_enabled?
# We are adding verification at the same time as replication, so we

View File

@ -327,6 +327,9 @@ That's all of the required database changes.
```
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#carrierwave_uploader` method which should return a `CarrierWave::Uploader`, and implement the class method `.model` to return the `CoolWidget` class:
- Implement the `replicable_title` and `replicable_title_plural` methods to
return the human-readable singular and pluralized title of the replicable,
which will be displayed in the UI and Rails console
```ruby
# frozen_string_literal: true
@ -340,6 +343,16 @@ That's all of the required database changes.
::CoolWidget
end
# @return [String] human-readable title.
def self.replicable_title
s_('Geo|Cool Widget')
end
# @return [String] pluralized human-readable title.
def self.replicable_title_plural
s_('Geo|Cool Widgets')
end
def carrierwave_uploader
model_record.file
end

View File

@ -182,7 +182,6 @@ Gitlab/BoundedContexts:
- 'app/graphql/mutations/incident_management/timeline_event_tag/base.rb'
- 'app/graphql/mutations/incident_management/timeline_event_tag/create.rb'
- 'app/graphql/mutations/issues/base.rb'
- 'app/graphql/mutations/issues/bulk_update.rb'
- 'app/graphql/mutations/issues/common_mutation_arguments.rb'
- 'app/graphql/mutations/issues/create.rb'
- 'app/graphql/mutations/issues/link_alerts.rb'
@ -845,7 +844,6 @@ Gitlab/BoundedContexts:
- 'app/models/concerns/issuable.rb'
- 'app/models/concerns/issuable_link.rb'
- 'app/models/concerns/issue_available_features.rb'
- 'app/models/concerns/issue_parent.rb'
- 'app/models/concerns/issue_resource_event.rb'
- 'app/models/concerns/label_eventable.rb'
- 'app/models/concerns/legacy_bulk_insert.rb'
@ -2155,7 +2153,6 @@ Gitlab/BoundedContexts:
- 'ee/app/graphql/ee/mutations/alert_management/http_integration/http_integration_base.rb'
- 'ee/app/graphql/ee/mutations/alert_management/http_integration/update.rb'
- 'ee/app/graphql/ee/mutations/groups/update.rb'
- 'ee/app/graphql/ee/mutations/issues/bulk_update.rb'
- 'ee/app/graphql/ee/mutations/issues/create.rb'
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/mutations/resolves_issuable.rb'

View File

@ -12,7 +12,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'app/finders/merge_requests_finder.rb'
- 'app/finders/notes_finder.rb'
- 'app/finders/snippets_finder.rb'
- 'app/graphql/mutations/issues/bulk_update.rb'
- 'app/graphql/types/namespace_type.rb'
- 'app/graphql/types/project_type.rb'
- 'app/helpers/auto_devops_helper.rb'

View File

@ -58,29 +58,6 @@ Layout/LineBreakAfterFinalMixin:
- 'ee/app/workers/sync_seat_link_worker.rb'
- 'ee/app/workers/vulnerabilities/historical_statistics/deletion_worker.rb'
- 'ee/app/workers/vulnerabilities/statistics/schedule_worker.rb'
- 'ee/lib/api/dependency_proxy/packages/maven.rb'
- 'ee/lib/api/resource_iteration_events.rb'
- 'ee/lib/api/resource_weight_events.rb'
- 'ee/lib/ee/api/entities/project_push_rule.rb'
- 'ee/lib/gitlab/auth/group_saml/identity_linker.rb'
- 'ee/lib/gitlab/llm/chain/tools/explain_code/prompts/anthropic.rb'
- 'ee/lib/gitlab/search/zoekt/client.rb'
- 'ee/lib/gitlab/status_page/storage/s3_multipart_upload.rb'
- 'ee/lib/search/zoekt/query.rb'
- 'lib/api/discussions.rb'
- 'lib/api/events.rb'
- 'lib/api/group_labels.rb'
- 'lib/api/issues.rb'
- 'lib/api/labels.rb'
- 'lib/api/notes.rb'
- 'lib/api/project_events.rb'
- 'lib/api/remote_mirrors.rb'
- 'lib/api/resource_label_events.rb'
- 'lib/api/resource_milestone_events.rb'
- 'lib/api/resource_state_events.rb'
- 'lib/api/rubygem_packages.rb'
- 'lib/api/terraform/modules/v1/namespace_packages.rb'
- 'lib/api/terraform/modules/v1/project_packages.rb'
- 'lib/banzai/filter/references/design_reference_filter.rb'
- 'lib/feature.rb'
- 'lib/gitlab/auth/current_user_mode.rb'

View File

@ -249,7 +249,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/update_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/epics/create_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/epics/update_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/issues/promote_to_epic_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/issues/set_epic_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/issues/set_weight_spec.rb'
@ -531,7 +530,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'spec/requests/api/graphql/mutations/achievements/update_user_achievement_priorities_spec.rb'
- 'spec/requests/api/graphql/mutations/ci/runner/create_spec.rb'
- 'spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb'
- 'spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
- 'spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb'
- 'spec/requests/api/graphql/mutations/ml/models/delete_spec.rb'
- 'spec/requests/api/graphql/mutations/uploads/delete_spec.rb'

View File

@ -2255,7 +2255,6 @@ Style/InlineDisableAnnotation:
- 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
- 'spec/requests/api/graphql/groups_query_spec.rb'
- 'spec/requests/api/graphql/issues_spec.rb'
- 'spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
- 'spec/requests/api/group_import_spec.rb'
- 'spec/requests/api/internal/base_spec.rb'
- 'spec/requests/api/ml_model_packages_spec.rb'

View File

@ -1 +1 @@
dad1421c02c5d89c3d667ed0abd41da2336d6418
124a10130bac885023b44616e773ffd4229ee592

View File

@ -4,7 +4,6 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'
import EmailParticipantsWarning from './email_participants_warning.vue';
const ATTACHMENT_REGEXP = /!?\[.*?\]\(\/uploads\/[0-9a-f]{32}\/.*?\)/;
const DEFAULT_NOTEABLE_TYPE = 'Issue';
export default {
i18n: {
@ -34,11 +33,6 @@ export default {
type: Object,
required: true,
},
noteableType: {
type: String,
required: false,
default: DEFAULT_NOTEABLE_TYPE,
},
withAlertContainer: {
type: Boolean,
required: false,
@ -92,9 +86,6 @@ export default {
class="-gl-mb-3 gl-rounded-lg gl-rounded-b-none gl-pb-5 gl-pt-4"
:is-locked="isLocked"
:is-confidential="isConfidential"
:noteable-type="noteableType"
:locked-noteable-docs-path="noteableData.locked_discussion_docs_path"
:confidential-noteable-docs-path="noteableData.confidential_issues_docs_path"
/>
<slot></slot>
<div

View File

@ -399,7 +399,6 @@ export default {
:is-internal-note="noteIsInternal"
:note="note"
:noteable-data="getNoteableData"
:noteable-type="noteableType"
>
<markdown-editor
ref="markdownEditor"

View File

@ -1,5 +1,6 @@
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
computed: {
@ -11,7 +12,9 @@ export default {
return this.getNoteableDataByProp('archived_project_docs_path');
},
lockedIssueDocsPath() {
return this.getNoteableDataByProp('locked_discussion_docs_path');
return helpPagePath('user/discussions/_index.md', {
anchor: 'prevent-comments-by-locking-the-discussion',
});
},
},
methods: {

View File

@ -93,8 +93,6 @@ export default {
discussion_locked: false,
confidential: false,
issue_email_participants: [],
locked_discussion_docs_path: '',
confidential_issues_docs_path: '',
},
};
},
@ -266,7 +264,6 @@ export default {
:is-internal-note="noteIsInternal"
:note="note"
:noteable-data="noteableData"
:noteable-type="noteableType"
>
<markdown-editor
ref="markdownEditor"

View File

@ -1,6 +1,6 @@
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getStandardContext from './get_standard_context';
import { validateEvent } from './utils';
import { isEventEligible, validateEvent } from './utils';
export function dispatchSnowplowEvent(
category = document.body.dataset.page,
@ -12,6 +12,10 @@ export function dispatchSnowplowEvent(
throw new Error('Tracking: no category provided for tracking.');
}
if (!isEventEligible(action)) {
return false;
}
validateEvent(action);
const { label, property, extra = {} } = data;

View File

@ -38,35 +38,36 @@ export function initDefaultTrackers() {
return;
}
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
// must be before initializing the trackers
Tracking.setAnonymousUrls();
window.snowplow('enableActivityTracking', {
minimumVisitLength: 30,
heartbeatDelay: 30,
});
// must be after enableActivityTracking
const standardContext = getStandardContext();
const experimentContexts = getAllExperimentContexts();
// To not expose personal identifying information, the page title is hardcoded as `GitLab`
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/345243
window.snowplow('trackPageView', {
title: 'GitLab',
context: [standardContext, ...experimentContexts],
});
window.snowplow('setDocumentTitle', 'GitLab');
if (window.snowplowOptions.formTracking) {
Tracking.enableFormTracking(opts.formTrackingConfig);
}
if (window.snowplowOptions.linkClickTracking) {
window.snowplow('enableLinkClickTracking', {
pseudoClicks: true,
if (!window.gl?.onlySendDuoEvents) {
window.snowplow('enableActivityTracking', {
minimumVisitLength: 30,
heartbeatDelay: 30,
});
// must be after enableActivityTracking
const standardContext = getStandardContext();
const experimentContexts = getAllExperimentContexts();
// To not expose personal identifying information, the page title is hardcoded as `GitLab`
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/345243
window.snowplow('trackPageView', {
title: 'GitLab',
context: [standardContext, ...experimentContexts],
});
window.snowplow('setDocumentTitle', 'GitLab');
if (window.snowplowOptions.formTracking) {
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
Tracking.enableFormTracking(opts.formTrackingConfig);
}
if (window.snowplowOptions.linkClickTracking) {
window.snowplow('enableLinkClickTracking', {
pseudoClicks: true,
context: [standardContext, ...experimentContexts],
});
}
}
Tracking.flushPendingEvents();

View File

@ -9,6 +9,7 @@ import {
validateAdditionalProperties,
getBaseAdditionalProperties,
getCustomAdditionalProperties,
isEventEligible,
} from './utils';
const elementsWithBinding = new WeakMap();
@ -25,6 +26,10 @@ const InternalEvents = {
*
*/
trackEvent(event, additionalProperties = {}, category = undefined) {
if (!isEventEligible(event)) {
return;
}
validateAdditionalProperties(additionalProperties);
const baseProperties = getBaseAdditionalProperties(additionalProperties);

View File

@ -1,17 +1,16 @@
<script>
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
const noteableTypeText = {
Issue: __('issue'),
Epic: __('epic'),
MergeRequest: __('merge request'),
Task: __('task'),
KeyResult: __('key result'),
Objective: __('objective'),
};
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
docsLinks: {
locked: helpPagePath('user/discussions/_index', {
anchor: 'prevent-comments-by-locking-the-discussion',
}),
confidential: helpPagePath('user/discussions/_index', {
anchor: 'comments-on-confidential-items',
}),
},
components: {
GlIcon,
GlLink,
@ -28,22 +27,6 @@ export default {
default: false,
required: false,
},
noteableType: {
type: String,
required: false,
// eslint-disable-next-line @gitlab/require-i18n-strings
default: 'Issue',
},
lockedNoteableDocsPath: {
type: String,
required: false,
default: '',
},
confidentialNoteableDocsPath: {
type: String,
required: false,
default: '',
},
},
computed: {
warningIcon() {
@ -55,19 +38,6 @@ export default {
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
noteableTypeText() {
return noteableTypeText[this.noteableType];
},
confidentialContextText() {
return sprintf(__('This is a confidential %{noteableTypeText}.'), {
noteableTypeText: this.noteableTypeText,
});
},
lockedContextText() {
return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
noteableTypeText: this.noteableTypeText,
});
},
},
};
</script>
@ -85,36 +55,44 @@ export default {
<gl-sprintf
:message="
__(
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}.',
'Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd} and discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. People without permission will never get a notification and won\'t be able to comment.',
)
"
>
<template #noteableTypeText>{{ noteableTypeText }}</template>
<template #confidentialLink="{ content }">
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.docsLinks.confidential" target="_blank">{{ content }}</gl-link>
</template>
<template #lockedLink="{ content }">
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ content }}</gl-link>
<gl-link :href="$options.docsLinks.locked" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
{{
__("People without permission will never get a notification and won't be able to comment.")
}}
</span>
<span v-else-if="isConfidential" ref="confidential">
{{ confidentialContextText }}
{{ __('People without permission will never get a notification.') }}
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{
__('Learn more.')
}}</gl-link>
<gl-sprintf
:message="
__(
'Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd}. People without permission will never get a notification.',
)
"
>
<template #confidentialLink="{ content }">
<gl-link :href="$options.docsLinks.confidential" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
<span v-else-if="isLocked" ref="locked">
{{ lockedContextText }}
{{ __('Only project members can comment.') }}
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more.') }}</gl-link>
<gl-sprintf
:message="
__('Discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. Only members can comment.')
"
>
<template #lockedLink="{ content }">
<gl-link :href="$options.docsLinks.locked" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</div>
</template>

View File

@ -33,7 +33,6 @@ import {
import { TYPENAME_MERGE_REQUEST, TYPENAME_VULNERABILITY } from '~/graphql_shared/constants';
import {
I18N_WORK_ITEM_ERROR_CREATING,
sprintfWorkItem,
i18n,
NAME_TO_TEXT_LOWERCASE_MAP,
NAME_TO_TEXT_MAP,
@ -396,21 +395,27 @@ export default {
return options;
},
createErrorText() {
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, this.selectedWorkItemTypeName);
return sprintf(I18N_WORK_ITEM_ERROR_CREATING, {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
});
},
createWorkItemText() {
return sprintfWorkItem(s__('WorkItem|Create %{workItemType}'), this.selectedWorkItemTypeName);
return sprintf(s__('WorkItem|Create %{workItemType}'), {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
});
},
makeConfidentialText() {
return sprintfWorkItem(
return sprintf(
s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to users having at least the Planner role',
),
this.selectedWorkItemTypeName,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName] },
);
},
titleText() {
return sprintfWorkItem(s__('WorkItem|New %{workItemType}'), this.selectedWorkItemTypeName);
return sprintf(s__('WorkItem|New %{workItemType}'), {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
});
},
canUpdate() {
return this.workItem?.userPermissions?.updateWorkItem;

View File

@ -4,7 +4,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
import { detectAndConfirmSensitiveTokens } from '~/lib/utils/secret_detection';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { STATE_OPEN, WORK_ITEM_TYPE_NAME_TASK, i18n } from '~/work_items/constants';
import { STATE_OPEN, i18n } from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import gfmEventHub from '~/vue_shared/components/markdown/eventhub';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@ -17,19 +17,6 @@ import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import workItemEmailParticipantsByIidQuery from '../../graphql/notes/work_item_email_participants_by_iid.query.graphql';
import { findEmailParticipantsWidget } from '../../utils';
const DOCS_WORK_ITEM_LOCKED_TASKS_PATH = helpPagePath('user/tasks.html', {
anchor: 'lock-discussion',
});
const DOCS_WORK_ITEM_CONFIDENTIAL_TASKS_PATH = helpPagePath('user/tasks.html', {
anchor: 'confidential-tasks',
});
const DOCS_WORK_ITEM_LOCKED_OKRS_PATH = helpPagePath('user/okrs.html', {
anchor: 'lock-discussion',
});
const DOCS_WORK_ITEM_CONFIDENTIAL_OKRS_PATH = helpPagePath('user/okrs.html', {
anchor: 'confidential-okrs',
});
export default {
i18n: {
internal: s__('Notes|Make this an internal note'),
@ -201,23 +188,11 @@ export default {
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
docsLinks() {
return this.workItemType === WORK_ITEM_TYPE_NAME_TASK
? {
confidential_issues_docs_path: DOCS_WORK_ITEM_CONFIDENTIAL_TASKS_PATH,
locked_discussion_docs_path: DOCS_WORK_ITEM_LOCKED_TASKS_PATH,
}
: {
confidential_issues_docs_path: DOCS_WORK_ITEM_CONFIDENTIAL_OKRS_PATH,
locked_discussion_docs_path: DOCS_WORK_ITEM_LOCKED_OKRS_PATH,
};
},
getWorkItemData() {
return {
confidential: this.isWorkItemConfidential,
discussion_locked: this.isDiscussionLocked,
issue_email_participants: this.emailParticipants,
...this.docsLinks,
};
},
workItemTypeKey() {
@ -369,7 +344,6 @@ export default {
:is-internal-note="isDiscussionInternal || isNoteInternal"
:note="commentText"
:noteable-data="getWorkItemData"
:noteable-type="workItemTypeKey"
>
<markdown-editor
ref="markdownEditor"

View File

@ -9,7 +9,6 @@ import {
NAME_TO_TEXT_LOWERCASE_MAP,
NAME_TO_TEXT_MAP,
ALLOWED_CONVERSION_TYPES,
sprintfWorkItem,
WIDGET_TYPE_DESIGNS,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_MILESTONE,
@ -357,12 +356,14 @@ export default {
const isEpicWithSubepicsFeature =
this.parentWorkItemType === WORK_ITEM_TYPE_NAME_EPIC && this.hasSubepicsFeature;
if (this.hasParent && !isEpicWithSubepicsFeature) {
this.warningMessage = sprintfWorkItem(
this.warningMessage = sprintf(
s__(
'WorkItem|Parent item type %{parentWorkItemType} is not supported on %{workItemType}. Remove the parent item to change type.',
),
this.selectedWorkItemType.name,
this.parentWorkItemType,
{
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemType.name],
parentWorkItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.parentWorkItemType],
},
);
this.changeTypeDisabled = true;
@ -395,11 +396,11 @@ export default {
// Compare the widget definitions of both types
if (this.hasWidgetDifference) {
this.warningMessage = sprintfWorkItem(
this.warningMessage = sprintf(
s__(
'WorkItem|Some fields are not present in %{workItemType}. If you change type now, this information will be lost.',
),
this.selectedWorkItemType.name,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemType.name] },
);
}
},

View File

@ -2,12 +2,12 @@
import { GlDatepicker, GlFormGroup } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { findStartAndDueDateWidget, newWorkItemId } from '~/work_items/utils';
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import { formatDate, newDate, toISODateFormat } from '~/lib/utils/datetime_utility';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
NAME_TO_TEXT_LOWERCASE_MAP,
TRACKING_CATEGORY_SHOW,
WIDGET_TYPE_START_AND_DUE_DATE,
} from '../constants';
@ -215,7 +215,9 @@ export default {
}
})
.catch((error) => {
const message = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
const message = sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
});
this.$emit('error', message);
Sentry.captureException(error);
})

View File

@ -4,7 +4,7 @@ import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/alert';
import {
sprintfWorkItem,
NAME_TO_TEXT_LOWERCASE_MAP,
WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_SOURCE,
WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_BRANCH,
} from '~/work_items/constants';
@ -19,7 +19,7 @@ import {
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import getProjectRootRef from '~/work_items/graphql/get_project_root_ref.query.graphql';
import { s__, __ } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import confidentialMergeRequestState from '~/confidential_merge_request/state';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
@ -250,9 +250,9 @@ export default {
this.$emit('hideModal');
} catch {
createAlert({
message: sprintfWorkItem(
message: sprintf(
s__('WorkItem|Failed to create a branch for this %{workItemType}. Please try again.'),
this.workItemType?.toLowerCase(),
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
),
});
} finally {

View File

@ -2,11 +2,15 @@
import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { ERROR_POLICY_ALL } from '~/lib/graphql';
import { s__, __ } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import workItemDevelopmentQuery from '~/work_items/graphql/work_item_development.query.graphql';
import workItemDevelopmentUpdatedSubscription from '~/work_items/graphql/work_item_development.subscription.graphql';
import { sprintfWorkItem, STATE_OPEN, DEVELOPMENT_ITEMS_ANCHOR } from '~/work_items/constants';
import {
DEVELOPMENT_ITEMS_ANCHOR,
NAME_TO_TEXT_LOWERCASE_MAP,
STATE_OPEN,
} from '~/work_items/constants';
import { findDevelopmentWidget } from '~/work_items/utils';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
@ -109,21 +113,21 @@ export default {
},
openStateText() {
return this.closingMergeRequests.length > 1
? sprintfWorkItem(
? sprintf(
s__(
'WorkItem|This %{workItemType} will be closed when any of the following is merged.',
),
this.workItemType,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
)
: sprintfWorkItem(
: sprintf(
s__('WorkItem|This %{workItemType} will be closed when the following is merged.'),
this.workItemType,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
);
},
closedStateText() {
return sprintfWorkItem(
return sprintf(
s__('WorkItem|The %{workItemType} was closed automatically when a branch was merged.'),
this.workItemType,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
);
},
tooltipText() {

View File

@ -1,12 +1,12 @@
<script>
import { GlDisclosureDropdown, GlPopover, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
import {
NAME_TO_TEXT_LOWERCASE_MAP,
WORK_ITEM_TYPE_NAME_EPIC,
WORK_ITEM_TYPE_NAME_OBJECTIVE,
sprintfWorkItem,
} from '../../constants';
export default {
@ -42,11 +42,11 @@ export default {
},
methods: {
getPopoverText(workItemType) {
return sprintfWorkItem(
return sprintf(
s__(
'WorkItem|You cannot add another child %{workItemType}. Youve reached the maximum number of nested levels.',
),
workItemType,
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[workItemType] },
);
},
getPopoverLink(workItemType) {

View File

@ -4,7 +4,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { newWorkItemId } from '~/work_items/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import { MILESTONE_STATE } from '~/sidebar/constants';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
@ -14,7 +14,7 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
NAME_TO_TEXT_LOWERCASE_MAP,
TRACKING_CATEGORY_SHOW,
} from '../constants';
@ -218,7 +218,9 @@ export default {
})
.catch((error) => {
this.localMilestone = this.workItemMilestone;
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
const msg = sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
});
this.$emit('error', msg);
Sentry.captureException(error);
})

View File

@ -2,7 +2,7 @@
import { GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import updateParentMutation from '~/work_items/graphql/update_parent.mutation.graphql';
import { isValidURL } from '~/lib/utils/url_utility';
@ -22,8 +22,8 @@ import workItemAllowedParentTypesQuery from '../graphql/work_item_allowed_parent
import {
I18N_WORK_ITEM_ERROR_UPDATING,
NAME_TO_ENUM_MAP,
NAME_TO_TEXT_LOWERCASE_MAP,
NO_WORK_ITEM_IID,
sprintfWorkItem,
WORK_ITEM_TYPE_ENUM_EPIC,
WORK_ITEM_TYPE_NAME_EPIC,
WORK_ITEM_TYPE_NAME_ISSUE,
@ -249,7 +249,9 @@ export default {
.catch((error) => {
this.$emit(
'error',
sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType),
sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
}),
);
Sentry.captureException(error);
})
@ -289,7 +291,12 @@ export default {
this.localSelectedItem = this.parent?.id || NO_WORK_ITEM_IID;
}
} catch (error) {
this.$emit('error', sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType));
this.$emit(
'error',
sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
}),
);
Sentry.captureException(error);
} finally {
this.searchStarted = false;

View File

@ -1,125 +0,0 @@
# frozen_string_literal: true
module Mutations
module Issues
class BulkUpdate < BaseMutation
graphql_name 'IssuesBulkUpdate'
include Gitlab::Graphql::Authorize::AuthorizeResource
MAX_ISSUES = 100
description 'Allows updating several properties for a set of issues. ' \
'Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.'
argument :parent_id, ::Types::GlobalIDType[::IssueParent],
required: true,
description: 'Global ID of the parent to which the bulk update will be scoped. ' \
'The parent can be a project. The parent can also be a group (Premium and Ultimate only). ' \
'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.'
argument :ids, [::Types::GlobalIDType[::Issue]],
required: true,
description: 'Global ID array of the issues that will be updated. ' \
"IDs that the user can\'t update will be ignored. A max of #{MAX_ISSUES} can be provided."
argument :assignee_ids, [::Types::GlobalIDType[::User]],
required: false,
description: 'Global ID array of the users that will be assigned to the given issues. ' \
'Existing assignees will be replaced with the ones on this list.'
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false,
description: 'Global ID of the milestone that will be assigned to the issues.'
argument :state_event, Types::IssueStateEventEnum,
description: 'Close or reopen an issue.',
required: false
argument :add_label_ids, [::Types::GlobalIDType[::Label]],
description: 'Global ID array of the labels that will be added to the issues. ',
required: false
argument :remove_label_ids, [::Types::GlobalIDType[::Label]],
description: 'Global ID array of the labels that will be removed from the issues. ',
required: false
argument :subscription_event, Types::IssuableSubscriptionEventEnum,
description: 'Subscribe to or unsubscribe from issue notifications.',
required: false
field :updated_issue_count, GraphQL::Types::Int,
null: true,
description: 'Number of issues that were successfully updated.'
def ready?(**args)
if Feature.disabled?(:bulk_update_issues_mutation)
raise_resource_not_available_error! '`bulk_update_issues_mutation` feature flag is disabled.'
end
if args[:ids].size > MAX_ISSUES
raise Gitlab::Graphql::Errors::ArgumentError,
format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: MAX_ISSUES)
end
super
end
def resolve(ids:, parent_id:, **attributes)
parent = find_parent!(parent_id)
result = Issuable::BulkUpdateService.new(
parent,
current_user,
prepared_params(attributes, ids)
).execute('issue')
if result.success?
{ updated_issue_count: result.payload[:count], errors: [] }
else
{ errors: result.errors }
end
end
private
def find_parent!(parent_id)
parent = GitlabSchema.find_by_gid(parent_id).sync
raise_resource_not_available_error! unless current_user.can?("read_#{parent.to_ability_name}", parent)
parent
end
def prepared_params(attributes, ids)
prepared = attributes.except(*global_id_arguments).merge(issuable_ids: model_ids_from(ids).uniq)
global_id_arguments.each do |argument|
next unless attributes.key?(argument)
prepared[argument] = model_ids_from(attributes[argument])
end
# Ruby 3+: This can be simplified to:
# prepared.transform_keys(param_mappings)
prepared.transform_keys { |key| param_mappings[key] || key }
end
def param_mappings
{}
end
def global_id_arguments
%i[assignee_ids milestone_id add_label_ids remove_label_ids]
end
def model_ids_from(attributes)
return if attributes.nil?
return attributes.map(&:model_id) if attributes.is_a?(Array)
attributes.model_id
end
end
end
end
Mutations::Issues::BulkUpdate.prepend_mod

View File

@ -9,8 +9,7 @@ module Mutations
MAX_WORK_ITEMS = 100
description 'Allows updating several properties for a set of issues. ' \
'Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.'
description 'Allows updating several properties for a set of work items. '
argument :ids, [::Types::GlobalIDType[::WorkItem]],
required: true,

View File

@ -87,7 +87,6 @@ module Types
mount_mutation Mutations::Issues::Move
mount_mutation Mutations::Issues::LinkAlerts
mount_mutation Mutations::Issues::UnlinkAlert
mount_mutation Mutations::Issues::BulkUpdate, experiment: { milestone: '15.9' }
mount_mutation Mutations::Labels::Create
mount_mutation Mutations::Members::Groups::BulkUpdate
mount_mutation Mutations::Members::Projects::BulkUpdate

View File

@ -97,13 +97,13 @@ class ApplicationSetting < ApplicationRecord
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, type: Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_allowlist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown

View File

@ -19,7 +19,7 @@ class AuditEvent < ApplicationRecord
partitioned_by :created_at, strategy: :monthly
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :details, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events

View File

@ -26,7 +26,7 @@ module Ci
# rubocop:disable Cop/ActiveRecordSerialize
serialize :options
serialize :yaml_variables, ::Gitlab::Serializer::Ci::Variables
serialize :yaml_variables, coder: ::Gitlab::Serializer::Ci::Variables
# rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do

View File

@ -143,7 +143,7 @@ module Ci
delegate :enable_debug_trace!, to: :metadata
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, coder: Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
delegate :name, to: :project, prefix: true

View File

@ -28,7 +28,7 @@ module AuditEvents
partitioned_by :created_at, strategy: :monthly
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize -- We need this to serialize details stored in audit event.
serialize :details, type: Hash # rubocop:disable Cop/ActiveRecordSerialize -- We need this to serialize details stored in audit event.
belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events

View File

@ -7,9 +7,9 @@ module DiffPositionableNote
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :should_update_position?, unless: :importing?
serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :original_position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :change_position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
validate :diff_refs_match_commit, if: :for_commit?
validates :position, json_schema: { filename: "position", hash_conversion: true }

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
# == IssuParent
#
# Used as a common ancestor for Group and Project so we can allow a polymorphic
# Types::GlobalIDType[::IssueParent] in the GraphQL API
#
# Used by Project, Group
#
module IssueParent
end

View File

@ -17,9 +17,9 @@ class WebHookLog < ApplicationRecord
belongs_to :web_hook
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :request_data, Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :response_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :request_headers, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :request_data, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :response_headers, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
validates :web_hook, presence: true

View File

@ -233,13 +233,9 @@ class Issue < ApplicationRecord
scope :confidential_only, -> { where(confidential: true) }
scope :without_hidden, -> {
if Feature.enabled?(:optimize_issues_banned_users_query, :instance)
# We add `+ 0` to the author_id to make the query planner use a nested loop and prevent
# loading of all banned user IDs for certain queries
where_not_exists(Users::BannedUser.where('banned_users.user_id = (issues.author_id + 0)'))
else
where_not_exists(Users::BannedUser.where('banned_users.user_id = issues.author_id'))
end
# We add `+ 0` to the author_id to make the query planner use a nested loop and prevent
# loading of all banned user IDs for certain queries
where_not_exists(Users::BannedUser.where('banned_users.user_id = (issues.author_id + 0)'))
}
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }

View File

@ -155,7 +155,7 @@ class MergeRequest < ApplicationRecord
:sha,
:skip_ci
].freeze
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
serialize :merge_params, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
before_validation :set_draft_status

View File

@ -35,7 +35,7 @@ class PersonalAccessToken < ApplicationRecord
MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS_BUFFERED = 400
MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS = 365
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :scopes, type: Array # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user
belongs_to :organization, class_name: 'Organizations::Organization'

View File

@ -41,7 +41,6 @@ class Project < ApplicationRecord
include RunnerTokenExpirationInterval
include BlocksUnsafeSerialization
include Subquery
include IssueParent
include WorkItems::Parent
include UpdatedAtFilterable
include CrossDatabaseIgnoredTables

View File

@ -26,7 +26,7 @@ class ProjectImportData < ApplicationRecord
# because the credentials are necessary for a successful import.
# This is safe because the serialization is only going between rails
# and the database, never to any end users.
serialize :data, Serializers::UnsafeJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :data, coder: Serializers::UnsafeJson # rubocop:disable Cop/ActiveRecordSerialize
validates :project, presence: true

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Projects
class DeletionSchedule < ApplicationRecord
self.table_name = 'project_deletion_schedules'
belongs_to :project
belongs_to :deleting_user, foreign_key: 'user_id', class_name: 'User', inverse_of: :project_deletion_schedules
validates :marked_for_deletion_at, presence: true
end
end

View File

@ -112,7 +112,7 @@ class User < ApplicationRecord
devise :two_factor_backupable, otp_number_of_backup_codes: 10
devise :two_factor_backupable_pbkdf2
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
serialize :otp_backup_codes, coder: JSON # rubocop:disable Cop/ActiveRecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
@ -228,8 +228,9 @@ class User < ApplicationRecord
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :project_members
has_many :project_deletion_schedules, class_name: '::Projects::DeletionSchedule', inverse_of: :deleting_user
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :created_namespace_details, foreign_key: :creator_id, class_name: 'Namespace::Detail'
has_many :projects_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :project_members, source: :project
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent

View File

@ -268,7 +268,17 @@ module Projects
::Ci::DestroySecureFileService.new(project, current_user).execute(secure_file)
end
deleted_count = ::CommitStatus.for_project(project).delete_all
delete_commit_statuses
end
def delete_commit_statuses
deleted_count = 0
loop do
deleted_rows = ::CommitStatus.for_project(project).limit(BATCH_SIZE).delete_all
deleted_count += deleted_rows
break if deleted_rows < BATCH_SIZE
end
Gitlab::AppLogger.info(
class: self.class.name,

View File

@ -87,6 +87,7 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/patch/database_config')
require_dependency Rails.root.join('lib/gitlab/patch/redis_cache_store')
require_dependency Rails.root.join('lib/gitlab/patch/old_redis_cache_store')
require_dependency Rails.root.join('lib/gitlab/pdf')
require_dependency Rails.root.join('lib/gitlab/exceptions_app')
unless ::Gitlab.next_rails?

View File

@ -1,9 +0,0 @@
---
name: prompt_migration_explain_vulnerability
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475046
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164210
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480824
milestone: '17.4'
group: group::custom models
type: beta
default_enabled: true

View File

@ -1,9 +0,0 @@
---
name: prompt_migration_merge_request_reader
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499593
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169942
rollout_issue_url: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/issues/595
milestone: '17.6'
group: group::custom models
type: beta
default_enabled: true

View File

@ -1,9 +0,0 @@
---
name: prompt_migration_summarize_comments
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475044
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163772
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480550
milestone: '17.4'
group: group::custom models
type: beta
default_enabled: true

View File

@ -1,9 +0,0 @@
---
name: prompt_migration_troubleshoot_job
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475045
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169944
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/524191
milestone: '17.6'
group: group::custom models
type: beta
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: bulk_update_issues_mutation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108505
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388472
milestone: '15.9'
type: development
group: group::project management
default_enabled: false

View File

@ -1,10 +0,0 @@
---
name: optimize_issues_banned_users_query
description: Optimize banned user clause for issue list queries
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/394980
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189141
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/537923
milestone: '18.0'
group: group::project management
type: gitlab_com_derisk
default_enabled: false

View File

@ -2,7 +2,7 @@
name: rapid_diffs_on_commit_show
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539577
milestone: '18.0'
group: group::code review
type: wip

View File

@ -2,7 +2,7 @@
name: rapid_diffs_on_compare_show
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539580
milestone: '18.0'
group: group::code review
type: wip

View File

@ -2,7 +2,7 @@
name: rapid_diffs_on_mr_creation
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539576
milestone: '18.0'
group: group::code review
type: wip

View File

@ -2,7 +2,7 @@
name: rapid_diffs_on_mr_show
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539581
milestone: '18.0'
group: group::code review
type: wip

View File

@ -18,8 +18,6 @@ silenced = Rails.env.production? && !Gitlab::Utils.to_boolean(ENV['GITLAB_LOG_DE
deprecators.silenced = silenced
ignored_warnings = [
/Your `secret_key_base` is configured in `Rails.application.secrets`, which is deprecated in favor of/,
/Please pass the (coder|class) as a keyword argument/,
/Support for `config.active_support.cache_format_version/
]

View File

@ -8,14 +8,6 @@ description: Email addresses for non-GitLab users added to issues as participant
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42943
milestone: '13.5'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
namespace_id:
references: namespaces
backfill_via:
parent:
foreign_key: issue_id
table: issues
sharding_key: namespace_id
belongs_to: issue
sharding_key:
namespace_id: namespaces
table_size: small
desired_sharding_key_migration_job_name: BackfillIssueEmailParticipantsNamespaceId

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddExternalControlNameToComplianceRequirementsControls < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.0'
def up
with_lock_retries do
add_column :compliance_requirements_controls, :external_control_name, :text, null: true
end
add_text_limit :compliance_requirements_controls, :external_control_name, 255
end
def down
with_lock_retries do
remove_column :compliance_requirements_controls, :external_control_name, if_exists: true
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddIssueEmailParticipantsNamespaceIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.0'
disable_ddl_transaction!
def up
add_not_null_constraint :issue_email_participants, :namespace_id
end
def down
remove_not_null_constraint :issue_email_participants, :namespace_id
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveRedundantUuidIndexFromVulnerabilityOccurrences < Gitlab::Database::Migration[2.3]
milestone '18.0'
disable_ddl_transaction!
INDEX_NAME = 'index_vulnerability_occurrences_on_uuid_1'
def up
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
end
def down
add_concurrent_index :vulnerability_occurrences, :uuid, unique: true, name: INDEX_NAME
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddIndexOnExternalControlNameScopedToRequirement < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.0'
TABLE_NAME = :compliance_requirements_controls
INDEX_NAME = :i_unique_external_control_name_per_requirement
def up
add_concurrent_index TABLE_NAME,
[:compliance_requirement_id, :external_control_name],
unique: true,
where: "external_control_name IS NOT NULL",
name: INDEX_NAME
end
def down
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
ec7459c400ccc8721e3a007ae48676df04803d8373cc807988d8d6da4623f541

View File

@ -0,0 +1 @@
d64f957096ec66a174af25b6e3e985f3230ab73f5993d3df1665c2ab6fbd01e3

View File

@ -0,0 +1 @@
13829ef575973386ba3ea01df90ee1c56d024ae10fb9f935a12b1df9fc19ce47

View File

@ -0,0 +1 @@
62f68a3e9319c6f36ad7bab652003316637bc9296d3d739bafaf6ecece3cba34

View File

@ -12519,8 +12519,10 @@ CREATE TABLE compliance_requirements_controls (
encrypted_secret_token bytea,
encrypted_secret_token_iv bytea,
external_url text,
external_control_name text,
CONSTRAINT check_110c87ed8d CHECK ((char_length(expression) <= 255)),
CONSTRAINT check_5020dd6745 CHECK ((char_length(external_url) <= 1024))
CONSTRAINT check_5020dd6745 CHECK ((char_length(external_url) <= 1024)),
CONSTRAINT check_e3c26a3c02 CHECK ((char_length(external_control_name) <= 255))
);
CREATE SEQUENCE compliance_requirements_controls_id_seq
@ -16065,7 +16067,8 @@ CREATE TABLE issue_email_participants (
updated_at timestamp with time zone NOT NULL,
email text NOT NULL,
namespace_id bigint,
CONSTRAINT check_2c321d408d CHECK ((char_length(email) <= 255))
CONSTRAINT check_2c321d408d CHECK ((char_length(email) <= 255)),
CONSTRAINT check_9d8a1ecc85 CHECK ((namespace_id IS NOT NULL))
);
CREATE SEQUENCE issue_email_participants_id_seq
@ -32939,6 +32942,8 @@ CREATE UNIQUE INDEX i_sbom_occurrences_vulnerabilities_on_occ_id_and_vuln_id ON
CREATE INDEX i_software_license_policies_on_custom_software_license_id ON software_license_policies USING btree (custom_software_license_id);
CREATE UNIQUE INDEX i_unique_external_control_name_per_requirement ON compliance_requirements_controls USING btree (compliance_requirement_id, external_control_name) WHERE (external_control_name IS NOT NULL);
CREATE INDEX i_vuln_occurrences_on_proj_report_loc_dep_pkg_ver_file_img ON vulnerability_occurrences USING btree (project_id, report_type, ((((location -> 'dependency'::text) -> 'package'::text) ->> 'name'::text)), (((location -> 'dependency'::text) ->> 'version'::text)), COALESCE((location ->> 'file'::text), (location ->> 'image'::text))) WHERE (report_type = ANY (ARRAY[2, 1]));
CREATE INDEX idx_abuse_reports_user_id_status_and_category ON abuse_reports USING btree (user_id, status, category);
@ -37817,8 +37822,6 @@ CREATE INDEX index_vulnerability_occurrences_on_location_k8s_cluster_id ON vulne
CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON vulnerability_occurrences USING btree (scanner_id);
CREATE UNIQUE INDEX index_vulnerability_occurrences_on_uuid_1 ON vulnerability_occurrences USING btree (uuid);
CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON vulnerability_occurrences USING btree (vulnerability_id);
CREATE INDEX index_vulnerability_occurrences_prim_iden_id_and_vuln_id ON vulnerability_occurrences USING btree (primary_identifier_id, vulnerability_id);

View File

@ -19,7 +19,7 @@ You can move all repositories managed by GitLab to another file system or anothe
The GitLab API is the recommended way to move Git repositories:
- Between servers.
- Between different storage.
- Between different storages.
- From single-node Gitaly to Gitaly Cluster.
For more information, see:

View File

@ -7409,42 +7409,6 @@ Input type: `IssueUnlinkAlertInput`
| <a id="mutationissueunlinkalerterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissueunlinkalertissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.issuesBulkUpdate`
Allows updating several properties for a set of issues. Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.
{{< details >}}
**Introduced** in GitLab 15.9.
**Status**: Experiment.
{{< /details >}}
Input type: `IssuesBulkUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesbulkupdateaddlabelids"></a>`addLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be added to the issues. |
| <a id="mutationissuesbulkupdateassigneeids"></a>`assigneeIds` | [`[UserID!]`](#userid) | Global ID array of the users that will be assigned to the given issues. Existing assignees will be replaced with the ones on this list. |
| <a id="mutationissuesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesbulkupdateepicid"></a>`epicId` {{< icon name="warning-solid" >}} | [`EpicID`](#epicid) | **Deprecated:** This will be replaced by WorkItem hierarchyWidget. Deprecated in GitLab 17.5. |
| <a id="mutationissuesbulkupdatehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Health status that will be assigned to the issues. |
| <a id="mutationissuesbulkupdateids"></a>`ids` | [`[IssueID!]!`](#issueid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
| <a id="mutationissuesbulkupdateiterationid"></a>`iterationId` | [`IterationID`](#iterationid) | Global ID of the iteration that will be assigned to the issues. |
| <a id="mutationissuesbulkupdatemilestoneid"></a>`milestoneId` | [`MilestoneID`](#milestoneid) | Global ID of the milestone that will be assigned to the issues. |
| <a id="mutationissuesbulkupdateparentid"></a>`parentId` | [`IssueParentID!`](#issueparentid) | Global ID of the parent to which the bulk update will be scoped. The parent can be a project. The parent can also be a group (Premium and Ultimate only). Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`. |
| <a id="mutationissuesbulkupdateremovelabelids"></a>`removeLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be removed from the issues. |
| <a id="mutationissuesbulkupdatestateevent"></a>`stateEvent` | [`IssueStateEvent`](#issuestateevent) | Close or reopen an issue. |
| <a id="mutationissuesbulkupdatesubscriptionevent"></a>`subscriptionEvent` | [`IssuableSubscriptionEvent`](#issuablesubscriptionevent) | Subscribe to or unsubscribe from issue notifications. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesbulkupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesbulkupdateupdatedissuecount"></a>`updatedIssueCount` | [`Int`](#int) | Number of issues that were successfully updated. |
### `Mutation.iterationCadenceCreate`
Input type: `IterationCadenceCreateInput`
@ -12584,7 +12548,7 @@ Input type: `WorkItemAddLinkedItemsInput`
### `Mutation.workItemBulkUpdate`
Allows updating several properties for a set of issues. Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.
Allows updating several properties for a set of work items.
{{< details >}}
**Introduced** in GitLab 17.4.
@ -43922,15 +43886,6 @@ State of a GitLab issue or merge request.
| <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. |
| <a id="issuablestateopened"></a>`opened` | In open state. |
### `IssuableSubscriptionEvent`
Values for subscribing and unsubscribing from issuables.
| Value | Description |
| ----- | ----------- |
| <a id="issuablesubscriptioneventsubscribe"></a>`SUBSCRIBE` | Subscribe to an issuable. |
| <a id="issuablesubscriptioneventunsubscribe"></a>`UNSUBSCRIBE` | Unsubscribe from an issuable. |
### `IssueCreationIterationWildcardId`
Iteration ID wildcard values for issue creation.
@ -46791,12 +46746,6 @@ A `IssueID` is a global ID. It is encoded as a string.
An example `IssueID` is: `"gid://gitlab/Issue/1"`.
### `IssueParentID`
A `IssueParentID` is a global ID. It is encoded as a string.
An example `IssueParentID` is: `"gid://gitlab/IssueParent/1"`.
### `IterationID`
A `IterationID` is a global ID. It is encoded as a string.

View File

@ -197,6 +197,17 @@ GitLab adds a system note to the page details.
You must unlock all locked discussions in closed issues or merge requests before you can
reopen the issue or merge request.
## Comments on confidential items
Only users with permission to access a confidential item receive notifications for comments on the item.
If the item was not previously confidential, users without access may appear as participants. These users do not receive notifications while the item is confidential.
Who can be notified:
- Users assigned to the item, regardless of role.
- Users who authored the item, if they have at least the Guest role.
- Users with at least a Planner role in the group or project the item belongs to.
## Add an internal note
{{< history >}}

View File

@ -3,6 +3,7 @@
module API
class Discussions < ::API::Base
include PaginationParams
helpers ::API::Helpers::NotesHelpers
helpers ::RendersNotes

View File

@ -4,6 +4,7 @@ module API
class Events < ::API::Base
include PaginationParams
include APIGuard
helpers ::API::Helpers::EventsHelpers
allow_access_with_scope :read_user, if: ->(request) { request.get? || request.head? }

View File

@ -3,6 +3,7 @@
module API
class GroupLabels < ::API::Base
include PaginationParams
helpers ::API::Helpers::LabelHelpers
before { authenticate! }

View File

@ -4,6 +4,7 @@ module API
class Issues < ::API::Base
include PaginationParams
include APIGuard
helpers Helpers::IssuesHelpers
helpers SpammableActions::CaptchaCheck::RestApiActionsSupport

View File

@ -3,6 +3,7 @@
module API
class Labels < ::API::Base
include PaginationParams
helpers ::API::Helpers::LabelHelpers
before { authenticate! }

View File

@ -4,6 +4,7 @@ module API
class Notes < ::API::Base
include PaginationParams
include APIGuard
helpers ::API::Helpers::NotesHelpers
before { authenticate! }

View File

@ -4,6 +4,7 @@ module API
class ProjectEvents < ::API::Base
include PaginationParams
include APIGuard
helpers ::API::Helpers::EventsHelpers
feature_category :user_profile

View File

@ -3,6 +3,7 @@
module API
class RemoteMirrors < ::API::Base
include PaginationParams
helpers Helpers::RemoteMirrorsHelpers
feature_category :source_code_management

View File

@ -3,6 +3,7 @@
module API
class ResourceLabelEvents < ::API::Base
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }

View File

@ -3,6 +3,7 @@
module API
class ResourceMilestoneEvents < ::API::Base
include PaginationParams
helpers ::API::Helpers::NotesHelpers
resource_milestone_events_tags = %w[resource_milestone_events]

View File

@ -3,6 +3,7 @@
module API
class ResourceStateEvents < ::API::Base
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }

View File

@ -5,6 +5,7 @@
module API
class RubygemPackages < ::API::Base
include ::API::Helpers::Authentication
helpers ::API::Helpers::PackagesHelpers
feature_category :package_registry

View File

@ -6,6 +6,7 @@ module API
module V1
class NamespacePackages < ::API::Base
include ::API::Helpers::Authentication
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers

View File

@ -6,6 +6,7 @@ module API
module V1
class ProjectPackages < ::API::Base
include ::API::Helpers::Authentication
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers

10
lib/gitlab/pdf.rb Normal file
View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require "prawn"
require "prawn-svg"
module Gitlab
module PDF
autoload :Header, 'gitlab/pdf/header'
end
end

79
lib/gitlab/pdf/header.rb Normal file
View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
require "prawn"
require "prawn-svg"
module Gitlab
module PDF
class Header
include Prawn::View
def self.render(pdf, page: 0, height: 50)
new(pdf, page, height).render
end
def initialize(pdf, page, height)
@pdf = pdf
@page = page
@height = height
@halfway_point = pdf.bounds.width / 2
end
def render
y = @pdf.bounds.top
@pdf.bounding_box([0, y], width: @pdf.bounds.right, height: @height) do
# The logo and GitLab text
@pdf.bounding_box([0, @pdf.bounds.top], width: @halfway_point, height: @height) do
logo_path = Rails.root.join('app/assets/images/gitlab_logo.png')
@pdf.image(
logo_path,
width: 21,
height: 21,
position: :left,
vposition: 6
)
@pdf.text_box(
"GitLab",
at: [25, @pdf.bounds.top],
width: 100,
height: 30,
valign: :center,
size: 24,
style: :bold
)
end
# Title (right side)
@pdf.bounding_box([@halfway_point, @pdf.bounds.top], width: @halfway_point, height: @height) do
@pdf.text_box(
"Vulnerability Summary | #{Date.current.strftime('%B %-d, %Y')} | #{@page}",
align: :right,
valign: :center,
size: 10
)
end
# Gradient bar using SVG
gradient_svg = <<~SVG
<svg width="#{@pdf.bounds.width}" height="10">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#d2afed;stop-opacity:1" />
<stop offset="25%" style="stop-color:#fa8bca;stop-opacity:1" />
<stop offset="50%" style="stop-color:#ff76a4;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fd6c30;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="10" fill="url(#grad)"/>
</svg>
SVG
# Position the gradient bar just below the header content
@pdf.svg gradient_svg, at: [0, @pdf.bounds.top - 45]
end
end
end
end
end

View File

@ -22595,6 +22595,9 @@ msgstr ""
msgid "Discuss a specific suggestion or question."
msgstr ""
msgid "Discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. Only members can comment."
msgstr ""
msgid "Discussion locked."
msgstr ""
@ -27101,6 +27104,12 @@ msgstr ""
msgid "Geo|Choose specific groups or storage shards"
msgstr ""
msgid "Geo|Ci Secure File"
msgstr ""
msgid "Geo|Ci Secure Files"
msgstr ""
msgid "Geo|Comma-separated, e.g. '1.1.1.1, 2.2.2.0/24'"
msgstr ""
@ -27122,6 +27131,12 @@ msgstr ""
msgid "Geo|Consult Geo troubleshooting information"
msgstr ""
msgid "Geo|Container Repositories"
msgstr ""
msgid "Geo|Container Repository"
msgstr ""
msgid "Geo|Container repositories synchronization concurrency limit"
msgstr ""
@ -27140,6 +27155,24 @@ msgstr ""
msgid "Geo|Database Replication status"
msgstr ""
msgid "Geo|Dependency Proxy Blob"
msgstr ""
msgid "Geo|Dependency Proxy Blobs"
msgstr ""
msgid "Geo|Dependency Proxy Manifest"
msgstr ""
msgid "Geo|Dependency Proxy Manifests"
msgstr ""
msgid "Geo|Design Management Repositories"
msgstr ""
msgid "Geo|Design Management Repository"
msgstr ""
msgid "Geo|Disabled"
msgstr ""
@ -27206,6 +27239,12 @@ msgstr ""
msgid "Geo|GraphQL ID: %{id}"
msgstr ""
msgid "Geo|Group Wiki Repositories"
msgstr ""
msgid "Geo|Group Wiki Repository"
msgstr ""
msgid "Geo|Groups to synchronize"
msgstr ""
@ -27230,6 +27269,12 @@ msgstr ""
msgid "Geo|Internal URL (optional)"
msgstr ""
msgid "Geo|Job Artifact"
msgstr ""
msgid "Geo|Job Artifacts"
msgstr ""
msgid "Geo|Last event ID"
msgstr ""
@ -27254,12 +27299,24 @@ msgstr ""
msgid "Geo|Learn more about Geo site statuses"
msgstr ""
msgid "Geo|Lfs Object"
msgstr ""
msgid "Geo|Lfs Objects"
msgstr ""
msgid "Geo|Limit the number of concurrent operations this secondary site can run in the background."
msgstr ""
msgid "Geo|Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
msgstr ""
msgid "Geo|Merge Request Diff"
msgstr ""
msgid "Geo|Merge Request Diffs"
msgstr ""
msgid "Geo|Minimum interval in days"
msgstr ""
@ -27305,15 +27362,45 @@ msgstr ""
msgid "Geo|Offline"
msgstr ""
msgid "Geo|Package File"
msgstr ""
msgid "Geo|Package Files"
msgstr ""
msgid "Geo|Pages Deployment"
msgstr ""
msgid "Geo|Pages Deployments"
msgstr ""
msgid "Geo|Pending"
msgstr ""
msgid "Geo|Pipeline Artifact"
msgstr ""
msgid "Geo|Pipeline Artifacts"
msgstr ""
msgid "Geo|Primary"
msgstr ""
msgid "Geo|Primary site"
msgstr ""
msgid "Geo|Project Repositories"
msgstr ""
msgid "Geo|Project Repository"
msgstr ""
msgid "Geo|Project Wiki Repositories"
msgstr ""
msgid "Geo|Project Wiki Repository"
msgstr ""
msgid "Geo|Projects in certain groups"
msgstr ""
@ -27440,6 +27527,12 @@ msgstr ""
msgid "Geo|Site's status was updated %{timeAgo}."
msgstr ""
msgid "Geo|Snippet Repositories"
msgstr ""
msgid "Geo|Snippet Repository"
msgstr ""
msgid "Geo|Started"
msgstr ""
@ -27467,6 +27560,12 @@ msgstr ""
msgid "Geo|Synchronization status"
msgstr ""
msgid "Geo|Terraform State Version"
msgstr ""
msgid "Geo|Terraform State Versions"
msgstr ""
msgid "Geo|The URL of the primary site that is used internally by the secondary sites."
msgstr ""
@ -27539,6 +27638,12 @@ msgstr ""
msgid "Geo|Updated %{timeAgo}"
msgstr ""
msgid "Geo|Upload"
msgstr ""
msgid "Geo|Uploads"
msgstr ""
msgid "Geo|Verification"
msgstr ""
@ -36489,6 +36594,12 @@ msgstr ""
msgid "Marked For Deletion At - %{deletion_time}"
msgstr ""
msgid "Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd} and discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. People without permission will never get a notification and won't be able to comment."
msgstr ""
msgid "Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd}. People without permission will never get a notification."
msgstr ""
msgid "Marked as draft. Can only be merged when marked as ready."
msgstr ""
@ -40241,9 +40352,6 @@ msgstr ""
msgid "No more than %{max_frameworks} compliance frameworks can be updated at the same time."
msgstr ""
msgid "No more than %{max_issues} issues can be updated at the same time"
msgstr ""
msgid "No more than %{max_work_items} work items can be modified at the same time."
msgstr ""
@ -43767,12 +43875,6 @@ msgstr ""
msgid "PendingMembers|There are no pending members left to approve. High five!"
msgstr ""
msgid "People without permission will never get a notification and won't be able to comment."
msgstr ""
msgid "People without permission will never get a notification."
msgstr ""
msgid "Percent rollout must be an integer number between 0 and 100"
msgstr ""
@ -61457,9 +61559,6 @@ msgstr ""
msgid "This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit})."
msgstr ""
msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}."
msgstr ""
msgid "This Cron pattern is invalid"
msgstr ""
@ -61745,9 +61844,6 @@ msgstr ""
msgid "This is a child pipeline within the parent pipeline"
msgstr ""
msgid "This is a confidential %{noteableTypeText}."
msgstr ""
msgid "This is a delayed job to run in %{remainingTime}"
msgstr ""

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :project_deletion_schedule, class: 'Projects::DeletionSchedule' do
association :project, factory: :project
association :deleting_user, factory: :user
marked_for_deletion_at { Time.current }
end
end

View File

@ -7,8 +7,6 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'
describe('Comment Field Layout Component', () => {
let wrapper;
const lockedDiscussionDocsPath = 'docs/locked/path';
const confidentialIssuesDocsPath = 'docs/confidential/path';
const noteWithAttachment =
'Have a look at this! ![image](/uploads/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/image.jpg)';
const attachmentMessage =
@ -19,8 +17,6 @@ describe('Comment Field Layout Component', () => {
const noteableDataMock = {
confidential: false,
discussion_locked: false,
locked_discussion_docs_path: lockedDiscussionDocsPath,
confidential_issues_docs_path: confidentialIssuesDocsPath,
};
const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning);
@ -75,8 +71,6 @@ describe('Comment Field Layout Component', () => {
expect(findIssuableNoteWarning().props()).toMatchObject({
isLocked: false,
isConfidential: true,
lockedNoteableDocsPath: lockedDiscussionDocsPath,
confidentialNoteableDocsPath: confidentialIssuesDocsPath,
});
});
});
@ -96,8 +90,6 @@ describe('Comment Field Layout Component', () => {
expect(findIssuableNoteWarning().props()).toMatchObject({
isConfidential: false,
isLocked: true,
lockedNoteableDocsPath: lockedDiscussionDocsPath,
confidentialNoteableDocsPath: confidentialIssuesDocsPath,
});
});
});

View File

@ -2,10 +2,16 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { dispatchSnowplowEvent } from '~/tracking/dispatch_snowplow_event';
import getStandardContext from '~/tracking/get_standard_context';
import { isEventEligible } from '~/tracking/utils';
import { extraContext, servicePingContext } from './mock_data';
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/tracking/get_standard_context');
jest.mock('~/tracking/utils', () => ({
...jest.requireActual('~/tracking/utils'),
isEventEligible: jest.fn(),
validateEvent: jest.fn(),
}));
const category = 'Incident Management';
const action = 'view_incident_details';
@ -20,6 +26,7 @@ describe('dispatchSnowplowEvent', () => {
beforeEach(() => {
snowplowMock.mockClear();
Sentry.captureException.mockClear();
isEventEligible.mockReturnValue(true);
});
it('calls snowplow trackStructEvent with correct arguments', () => {
@ -73,4 +80,24 @@ describe('dispatchSnowplowEvent', () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
});
it('returns false when event is not eligible', () => {
isEventEligible.mockReturnValue(false);
const result = dispatchSnowplowEvent(category, action, {});
expect(result).toBe(false);
expect(snowplowMock).not.toHaveBeenCalled();
});
it('returns true and tracks event when event is eligible', () => {
isEventEligible.mockReturnValue(true);
snowplowMock.mockImplementation(() => {});
const result = dispatchSnowplowEvent(category, action, {});
expect(result).toBe(true);
expect(snowplowMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -20,6 +20,7 @@ jest.mock('~/api', () => ({
jest.mock('~/tracking/utils', () => ({
...jest.requireActual('~/tracking/utils'),
getInternalEventHandlers: jest.fn(),
isEventEligible: jest.fn(),
}));
Tracker.enabled = jest.fn();
@ -43,6 +44,23 @@ describe('InternalEvents', () => {
describe('trackEvent', () => {
const category = 'TestCategory';
beforeEach(() => {
jest.spyOn(utils, 'isEventEligible').mockReturnValue(true);
});
it('does not track anything if event is not eligible', () => {
jest.spyOn(utils, 'isEventEligible').mockReturnValue(false);
jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
InternalEvents.trackEvent(event, allowedAdditionalProps, category);
expect(utils.isEventEligible).toHaveBeenCalledWith(event);
expect(Tracking.event).not.toHaveBeenCalled();
expect(API.trackInternalEvent).not.toHaveBeenCalled();
expect(InternalEvents.trackBrowserSDK).not.toHaveBeenCalled();
});
it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
InternalEvents.trackEvent(event, {}, category);

View File

@ -89,6 +89,7 @@ describe('Tracking', () => {
describe('initDefaultTrackers', () => {
beforeEach(() => {
window.gl.onlySendDuoEvents = false;
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
enableFormTracking = jest
@ -234,6 +235,24 @@ describe('Tracking', () => {
snowplowSpy.mock.invocationCallOrder[0],
);
});
it('does not initialize snowplow default trackers if `onlySendDuoEvents` is true', () => {
window.gl.onlySendDuoEvents = true;
initDefaultTrackers();
expect(snowplowSpy).not.toHaveBeenCalledWith('enableActivityTracking', expect.anything());
expect(snowplowSpy).not.toHaveBeenCalledWith('trackPageView', expect.anything());
expect(snowplowSpy).not.toHaveBeenCalledWith('setDocumentTitle', expect.anything());
expect(enableFormTracking).not.toHaveBeenCalled();
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking', expect.anything());
expect(bindDocumentSpy).toHaveBeenCalled();
expect(trackLoadEventsSpy).toHaveBeenCalled();
expect(bindInternalEventDocumentSpy).toHaveBeenCalled();
expect(trackInternalLoadEventsSpy).toHaveBeenCalled();
expect(initBrowserSDKSpy).toHaveBeenCalled();
});
});
describe('when there are experiment contexts', () => {
@ -249,6 +268,7 @@ describe('Tracking', () => {
];
beforeEach(() => {
window.gl.onlySendDuoEvents = false;
getAllExperimentContexts.mockReturnValue(experimentContexts);
window.snowplowOptions = {
...window.snowplowOptions,

View File

@ -1,48 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
exports[`Noteable Warning Component when item is locked but not confidential renders information about locked item 1`] = `
<span>
The discussion in this issue is locked. Only project members can comment.
Discussion is
<gl-link-stub
href="locked-path"
href="/help/user/discussions/_index#prevent-comments-by-locking-the-discussion"
target="_blank"
>
Learn more.
locked
</gl-link-stub>
. Only members can comment.
</span>
`;
exports[`Issue Warning Component when noteable is confidential but not locked renders information about confidential issue 1`] = `
exports[`Noteable Warning Component when noteable is confidential but not locked renders information about confidential item 1`] = `
<span>
This is a confidential issue. People without permission will never get a notification.
Marked as
<gl-link-stub
href="confidential-path"
href="/help/user/discussions/_index#comments-on-confidential-items"
target="_blank"
>
Learn more.
confidential
</gl-link-stub>
. People without permission will never get a notification.
</span>
`;
exports[`Issue Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
exports[`Noteable Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
<span>
<span>
This issue is
Marked as
<gl-link-stub
href=""
href="/help/user/discussions/_index#comments-on-confidential-items"
target="_blank"
>
confidential
</gl-link-stub>
and its
and discussion is
<gl-link-stub
href=""
href="/help/user/discussions/_index#prevent-comments-by-locking-the-discussion"
target="_blank"
>
discussion is locked
locked
</gl-link-stub>
.
. People without permission will never get a notification and won't be able to comment.
</span>
People without permission will never get a notification and won't be able to comment.
</span>
`;

View File

@ -3,7 +3,7 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
describe('Issue Warning Component', () => {
describe('Noteable Warning Component', () => {
let wrapper;
const findIcon = (w = wrapper) => w.findComponent(GlIcon);
@ -22,16 +22,15 @@ describe('Issue Warning Component', () => {
},
});
describe('when issue is locked but not confidential', () => {
describe('when item is locked but not confidential', () => {
beforeEach(() => {
wrapper = createComponent({
isLocked: true,
lockedNoteableDocsPath: 'locked-path',
isConfidential: false,
});
});
it('renders information about locked issue', () => {
it('renders information about locked item', () => {
expect(findLockedBlock().exists()).toBe(true);
expect(findLockedBlock().element).toMatchSnapshot();
});
@ -40,11 +39,11 @@ describe('Issue Warning Component', () => {
expect(findIcon().exists()).toBe(true);
});
it('does not render information about locked and confidential issue', () => {
it('does not render information about locked and confidential item', () => {
expect(findLockedAndConfidentialBlock().exists()).toBe(false);
});
it('does not render information about confidential issue', () => {
it('does not render information about confidential item', () => {
expect(findConfidentialBlock().exists()).toBe(false);
});
});
@ -54,16 +53,15 @@ describe('Issue Warning Component', () => {
wrapper = createComponent({
isLocked: false,
isConfidential: true,
confidentialNoteableDocsPath: 'confidential-path',
});
});
it('renders information about confidential issue', async () => {
it('renders information about confidential item', async () => {
expect(findConfidentialBlock().exists()).toBe(true);
expect(findConfidentialBlock().element).toMatchSnapshot();
await nextTick();
expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.');
expect(findConfidentialBlock(wrapper).text()).toContain('Marked as confidential.');
});
it('renders warning icon', () => {
@ -134,70 +132,13 @@ describe('Issue Warning Component', () => {
);
});
it('renders confidential & locked messages with noteable "issue"', () => {
expect(findLockedBlock(wrapperLocked).text()).toContain(
'The discussion in this issue is locked.',
);
it('renders confidential & locked messages with noteable', () => {
expect(findLockedBlock(wrapperLocked).text()).toContain('Discussion is locked.');
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential issue.',
'Marked as confidential.',
);
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
'This issue is confidential and its discussion is locked.',
);
});
it('renders confidential & locked messages with noteable "epic"', async () => {
wrapperLocked.setProps({
noteableType: 'Epic',
});
wrapperConfidential.setProps({
noteableType: 'Epic',
});
wrapperLockedAndConfidential.setProps({
noteableType: 'Epic',
});
await nextTick();
expect(findLockedBlock(wrapperLocked).text()).toContain(
'The discussion in this epic is locked.',
);
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential epic.',
);
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
'This epic is confidential and its discussion is locked.',
);
});
it('renders confidential & locked messages with noteable "merge request"', async () => {
wrapperLocked.setProps({
noteableType: 'MergeRequest',
});
wrapperConfidential.setProps({
noteableType: 'MergeRequest',
});
wrapperLockedAndConfidential.setProps({
noteableType: 'MergeRequest',
});
await nextTick();
expect(findLockedBlock(wrapperLocked).text()).toContain(
'The discussion in this merge request is locked.',
);
await nextTick();
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
'This is a confidential merge request.',
);
await nextTick();
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
'This merge request is confidential and its discussion is locked.',
'Marked as confidential and discussion is locked.',
);
});
});

View File

@ -166,12 +166,9 @@ describe('Work item comment form component', () => {
withAlertContainer: false,
noteableData: {
confidential: false,
confidential_issues_docs_path: '/help/user/tasks.html#confidential-tasks',
discussion_locked: false,
locked_discussion_docs_path: '/help/user/tasks.html#lock-discussion',
issue_email_participants: [],
},
noteableType: 'Task',
});
});

View File

@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::PDF::Header, feature_category: :vulnerability_management do
let(:pdf) { Prawn::Document.new }
let(:page_number) { '12345' }
let(:logo) { Rails.root.join('app/assets/images/gitlab_logo.png') }
describe '.render' do
subject(:render) { described_class.render(pdf, page: page_number, height: 123) }
let(:mock_instance) { instance_double(described_class) }
before do
allow(mock_instance).to receive(:render)
allow(described_class).to receive(:new).and_return(mock_instance)
end
it 'creates a new instance and calls render on it' do
render
expect(described_class).to have_received(:new).with(pdf, page_number, 123).once
expect(mock_instance).to have_received(:render).exactly(:once)
end
end
describe '#render' do
subject(:render_header) { described_class.render(pdf, page: page_number) }
before do
allow(pdf).to receive(:image).and_call_original
allow(pdf).to receive(:text_box).and_call_original
allow(pdf).to receive(:svg).and_call_original
end
it 'includes the gitlab logo in the header' do
render_header
expect(pdf).to have_received(:image).with(logo, any_args).once
end
it 'includes the gitlab name in the header' do
render_header
expect(pdf).to have_received(:text_box).with('GitLab', any_args).once
end
it 'includes the page number in the header' do
render_header
expect(pdf).to have_received(:text_box).with(/^.*#{page_number}/, any_args).once
end
it 'includes the svg divider in the header' do
render_header
expect(pdf).to have_received(:svg).with(%r{<svg.*</svg>}m, any_args).once
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::DeletionSchedule, type: :model, feature_category: :groups_and_projects do
subject(:project_deletion_schedule) { build(:project_deletion_schedule) }
describe 'validations' do
it { is_expected.to validate_presence_of(:marked_for_deletion_at) }
end
describe 'associations' do
it { is_expected.to belong_to(:project) }
specify do
expect(project_deletion_schedule).to belong_to(:deleting_user).class_name('User').with_foreign_key('user_id')
.inverse_of(:project_deletion_schedules)
end
end
end

View File

@ -231,6 +231,7 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
it { is_expected.to have_many(:admin_abuse_report_assignees).class_name('Admin::AbuseReportAssignee') }
it { is_expected.to have_many(:early_access_program_tracking_events).class_name('EarlyAccessProgram::TrackingEvent') }
it { is_expected.to have_many(:project_deletion_schedules).class_name('::Projects::DeletionSchedule').inverse_of(:deleting_user) }
describe '#triggers' do
let(:user) { create(:user) }

View File

@ -1,179 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Bulk update issues', feature_category: :team_planning do
include GraphqlHelpers
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group, developers: developer) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:label1) { create(:group_label, group: group) }
let_it_be(:label2) { create(:group_label, group: group) }
let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:parent) { project }
let(:max_issues) { Mutations::Issues::BulkUpdate::MAX_ISSUES }
let(:mutation) { graphql_mutation(:issues_bulk_update, base_arguments.merge(additional_arguments)) }
let(:mutation_response) { graphql_mutation_response(:issues_bulk_update) }
let(:current_user) { developer }
let(:base_arguments) { { parent_id: parent.to_gid.to_s, ids: updatable_issues.map { |i| i.to_gid.to_s } } }
let(:additional_arguments) do
{
assignee_ids: [current_user.to_gid.to_s],
milestone_id: milestone.to_gid.to_s,
state_event: :CLOSE,
add_label_ids: [label2.to_gid.to_s],
remove_label_ids: [label1.to_gid.to_s],
subscription_event: :UNSUBSCRIBE
}
end
before_all do
updatable_issues.each { |i| i.subscribe(developer, project) }
end
context 'when Gitlab is FOSS only' do
unless Gitlab.ee?
context 'when parent is a group' do
let(:parent) { group }
it 'does not allow bulk updating issues at the group level' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to contain_exactly(
hash_including(
'message' => match(/does not represent an instance of IssueParent/)
)
)
end
end
end
end
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
before do
stub_feature_flags(bulk_update_issues_mutation: false)
end
it 'returns a resource not available error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to contain_exactly(
hash_including(
'message' => '`bulk_update_issues_mutation` feature flag is disabled.'
)
)
end
end
context 'when user can not update all issues' do
let_it_be(:forbidden_issue) { create(:issue) }
it 'updates only issues that the user can update' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_issues.each(&:reset)
forbidden_issue.reset
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2).and(
not_change(forbidden_issue, :assignee_ids).from([])
)
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
)
end
end
context 'when user can update all issues' do
it 'updates all issues' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
.and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
.and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
.and(
change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
)
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
)
end
context 'when current user cannot read the specified project' do
let_it_be(:parent) { create(:project, :private) }
it 'returns a resource not found error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to contain_exactly(
hash_including(
'message' => "The resource that you are attempting to access does not exist or you don't have " \
'permission to perform this action'
)
)
end
end
context 'when setting arguments to null or none' do
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
before do
updatable_issues.each do |issue|
issue.update!(assignees: [current_user], milestone: milestone)
end
end
it 'updates all issues' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([current_user.id] * 2).to([])
.and(change { updatable_issues.map(&:milestone_id) }.from([milestone.id] * 2).to([nil] * 2))
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
)
end
end
end
context 'when update service returns an error' do
before do
allow_next_instance_of(Issuable::BulkUpdateService) do |update_service|
allow(update_service).to receive(:execute).and_return(
ServiceResponse.error(message: 'update error', http_status: 422) # rubocop:disable Gitlab/ServiceResponse
)
end
end
it 'returns an error message' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_data.dig('issuesBulkUpdate', 'errors')).to contain_exactly('update error')
end
end
context 'when trying to update more than the max allowed' do
before do
stub_const('Mutations::Issues::BulkUpdate::MAX_ISSUES', updatable_issues.count - 1)
end
it "restricts updating more than #{Mutations::Issues::BulkUpdate::MAX_ISSUES} issues at the same time" do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).to contain_exactly(
hash_including(
'message' =>
format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: max_issues)
)
)
end
end
end

View File

@ -868,6 +868,61 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
end
describe '#delete_commit_statuses' do
let(:service) { described_class.new(project, user, {}) }
let(:batch_size) { described_class::BATCH_SIZE }
context 'when there are no commit statuses' do
it 'does not delete anything and logs zero count' do
expect(Gitlab::AppLogger).to receive(:info).with(
class: described_class.name,
project_id: project.id,
message: 'leftover commit statuses',
orphaned_commit_status_count: 0
)
expect { service.send(:delete_commit_statuses) }.not_to change(::CommitStatus, :count)
end
end
context 'when there are fewer commit statuses than the batch size' do
before do
create_list(:commit_status, 2, project: project)
end
it 'deletes all statuses in a single batch and logs the count' do
expect(Gitlab::AppLogger).to receive(:info).with(
class: described_class.name,
project_id: project.id,
message: 'leftover commit statuses',
orphaned_commit_status_count: 2
)
expect { service.send(:delete_commit_statuses) }
.to change { CommitStatus.for_project(project).count }.from(2).to(0)
end
end
context 'when there are more commit statuses than the batch size' do
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
create_list(:commit_status, 3, project: project)
end
it 'deletes statuses in multiple batches and logs the total count' do
expect(Gitlab::AppLogger).to receive(:info).with(
class: described_class.name,
project_id: project.id,
message: 'leftover commit statuses',
orphaned_commit_status_count: 3
)
expect { service.send(:delete_commit_statuses) }
.to change { CommitStatus.for_project(project).count }.from(3).to(0)
end
end
end
def destroy_project(project, user, params = {})
described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
end

View File

@ -439,18 +439,18 @@ RSpec.shared_examples 'work items notifications' do
end
end
RSpec.shared_examples 'work items lock discussion' do |type|
RSpec.shared_examples 'work items lock discussion' do
it 'locks and unlocks discussion', :aggregate_failures do
click_button _('More actions'), match: :first
click_button 'Lock discussion'
click_button _('More actions'), match: :first # click again to close the dropdown
expect(page).to have_text "The discussion in this #{type} is locked. Only project members can comment."
expect(page).to have_text "Discussion is locked. Only members can comment."
click_button _('More actions'), match: :first
click_button 'Unlock discussion'
expect(page).not_to have_text "The discussion in this #{type} is locked. Only project members can comment."
expect(page).not_to have_text "Discussion is locked. Only members can comment."
end
end

Some files were not shown because too many files have changed in this diff Show More