Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-06 18:10:13 +00:00
parent fad8f90d7a
commit 23985334ba
66 changed files with 893 additions and 374 deletions

View File

@ -240,7 +240,6 @@
"WorkItemWidgetDefinition": [
"WorkItemWidgetDefinitionAssignees",
"WorkItemWidgetDefinitionGeneric",
"WorkItemWidgetDefinitionHealthStatus",
"WorkItemWidgetDefinitionHierarchy",
"WorkItemWidgetDefinitionLabels",
"WorkItemWidgetDefinitionWeight"

View File

@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/author.fragment.graphql"
query note($id: NoteID!) {
query noteForTooltip($id: NoteID!) {
note(id: $id) {
id
author {

View File

@ -818,7 +818,10 @@ export default {
this.viewType = ISSUES_LIST_VIEW_KEY;
},
handleSelectIssuable(issuable) {
this.activeIssuable = issuable;
this.activeIssuable = {
...issuable,
fullPath: this.fullPath,
};
},
updateIssuablesCache(workItem) {
const client = this.$apollo.provider.clients.defaultClient;

View File

@ -5,7 +5,6 @@ import errorTrackingStore from '~/error_tracking/store';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import initLinkedResources from '~/linked_resources';
import IssueApp from './components/app.vue';
import DescriptionComponent from './components/description.vue';
@ -73,8 +72,6 @@ export function initIssuableApp(store) {
bootstrapApollo({ ...issueState, issueType });
scrollToTargetOnResize();
if (issueType === TYPE_INCIDENT) {
initLinkedResources();
}

View File

@ -21,7 +21,7 @@ export default {
{ id: 0, href: this.switchDashboardPath, text: this.dashboardLinkText },
{
id: 1,
href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/460910',
href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/478844',
text: __('Provide feedback'),
extraAttrs: {
target: '__blank',

View File

@ -136,7 +136,7 @@ export default {
class="gl-relative"
data-testid="assigned-user"
>
<gl-avatar :src="user.avatarUrl" :size="32" />
<gl-avatar :src="user.avatarUrl" :size="32" class="!gl-bg-white" />
<span
v-if="isCurrentUser(user)"
class="gl-absolute -gl-left-2 -gl-top-2 gl-flex gl-h-5 gl-w-5 gl-items-center gl-justify-center gl-rounded-full gl-bg-blue-500 gl-p-1 gl-text-white"

View File

@ -90,15 +90,18 @@ export default {
},
canEdit: {
type: Boolean,
required: true,
required: false,
default: false,
},
canAwardEmoji: {
type: Boolean,
required: true,
required: false,
default: false,
},
canDelete: {
type: Boolean,
required: true,
required: false,
default: false,
},
canResolve: {
type: Boolean,

View File

@ -0,0 +1,134 @@
<script>
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_NOTE } from '~/graphql_shared/constants';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SafeHtml from '~/vue_shared/directives/safe_html';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import noteQuery from '../graphql/note.query.graphql';
import NoteEditedText from './note_edited_text.vue';
import NoteableNote from './noteable_note.vue';
export default {
components: {
NoteEditedText,
NoteableNote,
},
directives: {
SafeHtml,
},
mixins: [timeagoMixin, glFeatureFlagMixin()],
props: {
noteId: {
type: String,
required: false,
default: '',
},
},
data() {
return {
note: null,
hidden: false,
};
},
computed: {
showNote() {
return this.note && !this.hidden && !this.isSyntheticNote;
},
showEdited() {
return this.note && this.note.created_at !== this.note.last_edited_at;
},
isSyntheticNote() {
return Boolean(this.noteId?.match(/([a-f0-9]{40})/));
},
noteHtml() {
return this.note?.body_html;
},
},
watch: {
async noteHtml() {
try {
await this.$nextTick();
renderGFM(this.$refs.noteBody);
} catch {
this.fallback();
}
},
},
mounted() {
if (this.isSyntheticNote) {
this.fallback();
}
},
methods: {
fallback() {
this.hidden = true;
},
},
apollo: {
note: {
skip() {
return !this.noteId || this.isSyntheticNote;
},
query: noteQuery,
variables() {
return {
id: convertToGraphQLId(TYPENAME_NOTE, this.noteId),
};
},
update(data) {
return {
...data.note,
author: {
...data.note.author,
id: getIdFromGraphQLId(data.note.author.id),
},
last_edited_by: {
...data.note.last_edited_by,
id: getIdFromGraphQLId(data.note.last_edited_by?.id),
},
id: getIdFromGraphQLId(data.note.id),
};
},
result(result) {
if (result?.errors?.length > 0) {
Sentry.captureException(result.errors[0].message);
this.fallback();
}
if (!result?.data?.note) {
this.fallback();
}
},
error(error) {
Sentry.captureException(error);
this.fallback();
},
},
},
};
</script>
<template>
<noteable-note
v-if="showNote"
:id="`note_${noteId}`"
:note="note"
:show-reply-button="false"
should-scroll-to-note
>
<template #note-body>
<div ref="noteBody" class="note-body">
<div v-safe-html:[$options.safeHtmlConfig]="noteHtml" class="note-text md"></div>
<note-edited-text
v-if="showEdited"
:edited-at="note.last_edited_at"
:edited-by="note.last_edited_by"
:action-text="__('Edited')"
class="note_edited_ago"
/>
</div>
</template>
</noteable-note>
</template>

View File

@ -1,9 +1,9 @@
<script>
import { GlSprintf, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import $ from 'jquery';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
@ -126,6 +126,9 @@ export default {
author() {
return this.note.author;
},
authorId() {
return getIdFromGraphQLId(this.author.id);
},
commentType() {
return this.note.internal ? __('internal note') : __('comment');
},
@ -136,11 +139,17 @@ export default {
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
target: this.isTarget,
'is-editable': this.note.current_user.can_edit,
'is-editable': this.canEdit,
};
},
canAwardEmoji() {
return this.note.current_user?.can_award_emoji ?? false;
},
canEdit() {
return this.note.current_user?.can_edit ?? false;
},
canReportAsAbuse() {
return Boolean(this.reportAbusePath) && this.author.id !== this.getUserData.id;
return Boolean(this.reportAbusePath) && this.authorId !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@ -176,7 +185,7 @@ export default {
if (!this.discussionRoot) return false;
if (!this.note.resolvable) return false;
return this.note.current_user.can_resolve_discussion;
return this.note.current_user?.can_resolve_discussion;
},
lineRange() {
return this.note.position?.line_range;
@ -236,14 +245,14 @@ export default {
if (noteId === this.note.id) {
this.isEditing = true;
this.setSelectedCommentPositionHover();
this.scrollToNoteIfNeeded($(this.$el));
this.$el.scrollIntoView();
}
});
},
mounted() {
if (this.isTarget && this.shouldScrollToNote) {
this.scrollToNoteIfNeeded($(this.$el));
this.$el.scrollIntoView({ duration: 0 });
}
},
@ -253,7 +262,6 @@ export default {
'removeNote',
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
'updateAssignees',
'setSelectedCommentPositionHover',
]),
@ -428,7 +436,7 @@ export default {
<div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
<gl-avatar-link
:href="author.path"
:data-user-id="author.id"
:data-user-id="authorId"
:data-username="author.username"
class="js-user-link"
>
@ -446,7 +454,7 @@ export default {
<div v-else class="timeline-avatar gl-float-left">
<gl-avatar-link
:href="author.path"
:data-user-id="author.id"
:data-user-id="authorId"
:data-username="author.username"
class="js-user-link gl-relative"
>
@ -480,7 +488,7 @@ export default {
</note-header>
<note-actions
:author="author"
:author-id="author.id"
:author-id="authorId"
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
@ -489,9 +497,9 @@ export default {
:project-name="note.project_name"
:noteable-type="note.noteable_type"
:show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-edit="canEdit"
:can-award-emoji="canAwardEmoji"
:can-delete="canEdit"
:can-report-as-abuse="canReportAsAbuse"
:can-resolve="canResolve"
:resolvable="note.resolvable || note.isDraft"
@ -511,17 +519,19 @@ export default {
</div>
<div class="timeline-discussion-body">
<slot name="discussion-resolved-text"></slot>
<note-body
ref="noteBody"
:note="note"
:can-edit="note.current_user.can_edit"
:line="line"
:file="diffFile"
:is-editing="isEditing"
:help-page-path="helpPagePath"
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
/>
<slot name="note-body">
<note-body
ref="noteBody"
:note="note"
:can-edit="canEdit"
:line="line"
:file="diffFile"
:is-editing="isEditing"
:help-page-path="helpPagePath"
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
/>
</slot>
<div class="timeline-discussion-body-footer">
<slot name="after-note-body"></slot>
</div>

View File

@ -5,11 +5,13 @@ import { v4 as uuidv4 } from 'uuid';
import { InternalEvents } from '~/tracking';
import { getDraft, getAutoSaveKeyFromDiscussion } from '~/lib/utils/autosave';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { getLocationHash } from '~/lib/utils/url_utility';
import NotePreview from '~/notes/components/note_preview.vue';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
@ -26,6 +28,7 @@ import NotesActivityHeader from './notes_activity_header.vue';
export default {
name: 'NotesApp',
components: {
NotePreview,
NotesActivityHeader,
NoteableNote,
NoteableDiscussion,
@ -97,6 +100,7 @@ export default {
'userCanReply',
'sortDirection',
'timelineEnabled',
'targetNoteHash',
]),
sortDirDesc() {
return this.sortDirection === constants.DESC;
@ -104,6 +108,10 @@ export default {
noteableType() {
return this.noteableData.noteableType;
},
previewNoteId() {
if (!this.isLoading || !this.targetNoteHash) return null;
return this.targetNoteHash.replace('note_', '');
},
allDiscussions() {
let skeletonNotes = [];
@ -113,6 +121,17 @@ export default {
skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true,
});
if (
this.previewNoteId &&
!this.discussions.find((d) => d.notes[0].id === this.previewNoteId)
) {
const previewNote = {
id: this.previewNoteId,
isPreviewNote: true,
};
skeletonNotes.splice(prerenderedNotesCount / 2, 0, previewNote);
}
}
if (this.sortDirDesc) {
@ -172,6 +191,10 @@ export default {
window.addEventListener('hashchange', this.handleHashChanged);
if (this.targetNoteHash && this.targetNoteHash.startsWith('note_')) {
scrollToTargetOnResize();
}
eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
updated() {
@ -213,7 +236,7 @@ export default {
},
checkLocationHash() {
const hash = getLocationHash();
const noteId = hash && hash.replace(/^note_/, '');
const noteId = (hash && hash.startsWith('note_') && hash.replace(/^note_/, '')) ?? null;
if (noteId) {
const discussion = this.discussions.find((d) => d.notes.some(({ id }) => id === noteId));
@ -278,6 +301,13 @@ export default {
:key="discussion.id"
class="note-skeleton"
/>
<timeline-entry-item
v-else-if="discussion.isPreviewNote"
:key="discussion.id"
class="target note note-wrapper note-comment"
>
<note-preview :note-id="previewNoteId" />
</timeline-entry-item>
<timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
<draft-note :draft="discussion" />
</timeline-entry-item>

View File

@ -0,0 +1,26 @@
query snakeCaseNote($id: NoteID!) {
note(id: $id) {
id
author {
id
avatar_url: avatarUrl
name
username
web_url: webUrl
web_path: webPath
}
body_html: bodyHtml
created_at: createdAt
last_edited_at: lastEditedAt
last_edited_by: lastEditedBy {
id
avatar_url: avatarUrl
name
username
web_url: webUrl
web_path: webPath
}
internal
url
}
}

View File

@ -13,7 +13,7 @@ import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_co
import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '~/awards_handler';
import { isInViewport, scrollToElement, isInMRPage } from '~/lib/utils/common_utils';
import { isInMRPage } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
@ -632,12 +632,6 @@ export const toggleAwardRequest = ({ dispatch }, data) => {
});
};
export const scrollToNoteIfNeeded = (context, el) => {
if (!isInViewport(el[0])) {
scrollToElement(el);
}
};
export const fetchDiscussionDiffLines = ({ commit }, discussion) =>
axios.get(discussion.truncated_diff_lines_path).then(({ data }) => {
commit(types.SET_DISCUSSION_DIFF_LINES, {

View File

@ -116,11 +116,6 @@ export default {
default: '',
},
},
data() {
return {
showForm: this.isFormVisible || this.hasError,
};
},
computed: {
hasRelatedIssues() {
return this.relatedIssues.length > 0;
@ -176,29 +171,25 @@ export default {
: sprintf(this.$options.i18n.emptyItemsFree, { issuableType: this.issuableType });
},
},
mounted() {
this.$nextTick(() => {
if (this.showForm) {
this.$refs.relatedIssuesWidget.showForm();
}
});
},
updated() {
this.$nextTick(() => {
if (this.hasError) {
watch: {
isFormVisible(newVal) {
if (newVal === true) {
this.$refs.relatedIssuesWidget.showForm();
} else {
this.$refs.relatedIssuesWidget.hideForm();
}
});
},
},
mounted() {
if (this.isFormVisible) {
this.$refs.relatedIssuesWidget.showForm();
}
},
methods: {
handleFormSubmit(event) {
this.showForm = false;
this.$emit('addIssuableFormSubmit', event);
},
handleFormCancel(event) {
this.showForm = false;
this.$emit('addIssuableFormCancel', event);
this.$refs.relatedIssuesWidget.hideForm();
},
@ -227,6 +218,8 @@ export default {
:help-link-text="helpLinkText"
anchor-id="related-issues"
data-testid="related-issues-block"
@showForm="$emit('showForm')"
@hideForm="$emit('hideForm')"
>
<template #actions>
<slot name="header-actions"></slot>
@ -253,7 +246,7 @@ export default {
/>
</template>
<template v-if="!shouldShowTokenBody && !isFormVisible" #empty>
<template v-if="!shouldShowTokenBody" #empty>
<slot name="empty-state-message">{{ emptyStateMessage }}</slot>
<gl-link
v-if="hasHelpPath"

View File

@ -179,6 +179,7 @@ export default {
if (response && response.data && response.data.message) {
this.errorMessage = response.data.message;
}
this.isFormVisible = true;
})
.finally(() => {
this.isSubmitting = false;
@ -282,5 +283,7 @@ export default {
@addIssuableFormCancel="onPendingFormCancel"
@pendingIssuableRemoveRequest="onPendingIssueRemoveRequest"
@relatedIssueRemoveRequest="onRelatedIssueRemoveRequest"
@showForm="isFormVisible = true"
@hideForm="isFormVisible = false"
/>
</template>

View File

@ -108,9 +108,11 @@ export default {
showForm() {
this.isFormVisible = true;
this.collapsed = false;
this.$emit('showForm');
},
hideForm() {
this.isFormVisible = false;
this.$emit('hideForm');
},
toggleForm() {
if (this.isFormVisible) {

View File

@ -245,7 +245,7 @@ export default {
e.preventDefault();
this.$emit('select-issuable', {
iid: this.issuableIid,
webUrl: this.issuableLinkHref,
webUrl: this.issuable.webUrl,
fullPath: this.workItemFullPath,
});
},

View File

@ -1,6 +1,7 @@
<script>
import { GlLink, GlDrawer } from '@gitlab/ui';
import { GlLink, GlDrawer, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import { __ } from '~/locale';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
@ -8,9 +9,13 @@ import { visitUrl } from '~/lib/utils/url_utility';
export default {
name: 'WorkItemDrawer',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlLink,
GlDrawer,
GlButton,
WorkItemDetail: () => import('~/work_items/components/work_item_detail.vue'),
},
inject: ['fullPath'],
@ -31,6 +36,11 @@ export default {
default: TYPE_ISSUE,
},
},
data() {
return {
copyTooltipText: this.$options.i18n.copyTooltipText,
};
},
computed: {
activeItemFullPath() {
if (this.activeItem?.fullPath) {
@ -45,6 +55,10 @@ export default {
modalIsGroup() {
return this.issuableType === TYPE_EPIC;
},
headerReference() {
const path = this.activeItemFullPath.substring(this.activeItemFullPath.lastIndexOf('/') + 1);
return `${path}#${this.activeItem.iid}`;
},
},
methods: {
async deleteWorkItem({ workItemId }) {
@ -62,8 +76,12 @@ export default {
Sentry.captureException(error);
}
},
redirectToWorkItem() {
redirectToWorkItem(e) {
const workItem = this.activeItem;
if (e.metaKey || e.ctrlKey) {
return;
}
e.preventDefault();
const escapedFullPath = escapeRegExp(this.fullPath);
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
@ -80,6 +98,17 @@ export default {
visitUrl(workItem.webUrl);
}
},
handleCopyToClipboard() {
this.copyTooltipText = this.$options.i18n.copiedTooltipText;
setTimeout(() => {
this.copyTooltipText = this.$options.i18n.copyTooltipText;
}, 2000);
},
},
i18n: {
copyTooltipText: __('Copy item URL'),
copiedTooltipText: __('Copied'),
openTooltipText: __('Open in full page'),
},
};
</script>
@ -94,12 +123,39 @@ export default {
@close="$emit('close')"
>
<template #title>
<gl-link
class="gl-text-default"
:href="activeItem.webUrl"
@click.prevent="redirectToWorkItem"
>{{ __('Open full view') }}</gl-link
>
<div class="gl-text gl-flex gl-w-full gl-items-center gl-gap-x-2 xl:gl-px-4">
<gl-link
:href="activeItem.webUrl"
class="gl-text-sm gl-font-bold gl-text-default"
@click="redirectToWorkItem"
>
{{ headerReference }}
</gl-link>
<gl-button
v-gl-tooltip
data-testid="work-item-drawer-copy-button"
:title="copyTooltipText"
category="tertiary"
class="gl-text-secondary"
icon="link"
size="small"
:aria-label="$options.i18n.copyTooltipText"
:data-clipboard-text="activeItem.webUrl"
@click="handleCopyToClipboard"
/>
<gl-button
v-gl-tooltip
data-testid="work-item-drawer-link-button"
:href="activeItem.webUrl"
:title="$options.i18n.openTooltipText"
category="tertiary"
class="gl-text-secondary"
icon="maximize"
size="small"
:aria-label="$options.i18n.openTooltipText"
@click="redirectToWorkItem"
/>
</div>
</template>
<template #default>
<work-item-detail

View File

@ -107,7 +107,11 @@ $diff-file-header: 41px;
.image {
@apply gl-bg-strong;
text-align: center;
padding: 30px;
padding: $gl-padding;
&:has(> .view) {
padding-bottom: $gl-padding-32;
}
.wrap {
display: inline-block;

View File

@ -253,16 +253,6 @@ ul.related-merge-requests > li gl-emoji {
}
}
.issuable-header-slide-enter-active,
.issuable-header-slide-leave-active {
@apply gl-transition-all;
}
.issuable-header-slide-enter,
.issuable-header-slide-leave-to {
transform: translateY(-100%);
}
.issuable-sticky-header-visible {
--issuable-sticky-header-height: 40px;
}

View File

@ -17,13 +17,11 @@ module TimeTrackable
alias_method :time_spent?, :time_spent
attribute :time_estimate, default: 0
validate :check_time_estimate
validate :check_negative_time_spent
has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
after_initialize :set_time_estimate_default_value
before_save :set_time_estimate_default_value
after_save :clear_memoized_total_time_spent
end
@ -95,11 +93,18 @@ module TimeTrackable
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
def time_estimate
super || self.class.column_defaults['time_estimate']
end
def set_time_estimate_default_value
return if new_record?
return unless has_attribute?(:time_estimate)
# time estimate can be set to nil, in case of an invalid value, e.g. a String instead of a number, in which case
# we should not be overwriting it to default value, but rather have the validation catch the error
return if time_estimate_changed?
self.time_estimate ||= self.class.column_defaults['time_estimate']
self.time_estimate = self.class.column_defaults['time_estimate'] if read_attribute(:time_estimate).nil?
end
private
@ -136,8 +141,9 @@ module TimeTrackable
end
def check_time_estimate
return unless new_record? || time_estimate_changed?
return if time_estimate.is_a?(Numeric) && time_estimate >= 0
# we'll set the time_tracking to zero at DB level through default value
return unless time_estimate_changed?
return if read_attribute(:time_estimate).is_a?(Numeric) && read_attribute(:time_estimate) >= 0
errors.add(:time_estimate, _('must have a valid format and be greater than or equal to zero.'))
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ChangeTimeEstimateDefaultFromNullToZeroOnIssues < Gitlab::Database::Migration[2.2]
milestone '17.4'
enable_lock_retries!
TABLE_NAME = :issues
COLUMN_NAME = :time_estimate
def up
change_column_default(TABLE_NAME, COLUMN_NAME, from: nil, to: 0)
end
def down
remove_column_default(TABLE_NAME, COLUMN_NAME)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ChangeTimeEstimateDefaultFromNullToZeroOnMergeRequests < Gitlab::Database::Migration[2.2]
milestone '17.4'
enable_lock_retries!
TABLE_NAME = :merge_requests
COLUMN_NAME = :time_estimate
def up
change_column_default(TABLE_NAME, COLUMN_NAME, from: nil, to: 0)
end
def down
remove_column_default(TABLE_NAME, COLUMN_NAME)
end
end

View File

@ -0,0 +1 @@
39c3afa6ca1f4005d7d26d3ea510d406a2d4aa101bf5bf2208961b796d0d47bf

View File

@ -0,0 +1 @@
d7f9e2f68e4d0375e381f2345bc2b8fe4eac2f965fa467529d9a27c70e73f5a4

View File

@ -12367,7 +12367,7 @@ CREATE TABLE issues (
lock_version integer DEFAULT 0,
title_html text,
description_html text,
time_estimate integer,
time_estimate integer DEFAULT 0,
relative_position integer,
service_desk_reply_to character varying,
cached_markdown_version integer,
@ -13232,7 +13232,7 @@ CREATE TABLE merge_requests (
lock_version integer DEFAULT 0,
title_html text,
description_html text,
time_estimate integer,
time_estimate integer DEFAULT 0,
squash boolean DEFAULT false NOT NULL,
cached_markdown_version integer,
last_edited_at timestamp without time zone,

View File

@ -1,31 +0,0 @@
---
# Error: gitlab_base.CodeBlockNesting
#
# Ensures content nested in lists are spaced correctly.
#
extends: existence
message: "Items under an ordered list must be indented three spaces. Items under an unordered list must be indented two spaces."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/#nesting-inside-a-list-item
level: error
nonword: true
ignorecase: true
scope: raw
tokens:
- '^1. .*\n\n? ( )?[`\w-]'
- '^- .*\n\n? ( )?[`\w-]'
# Regex guide:
#
# "^1. .*" - Lines that start with an ordered list.
# "^- .*" - Lines that start with an unordered list.
#
# "\n\n?" - Then one or two newlines
#
# Ordered lists: " ( )?" - Two or four spaces (three = correct)
# Unordered lists: " ( )?" - One or three spaces (two = correct)
#
# "[`\w-]" - Any one of:
#
# - A backtick - For code blocks after a list.
# - A letter/number - For alert boxes, sentences, and nested ordered lists (after a list).
# - A hyphen - For nested unordered lists (after a list).

View File

@ -0,0 +1,31 @@
---
# Error: gitlab_base.ListIndentation
#
# Ensures content nested in lists is spaced correctly.
#
extends: existence
message: "Items nested under an ordered list must be indented three spaces. Items nested under an unordered list must be indented two spaces."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/#nesting-inside-a-list-item
level: error
nonword: true
ignorecase: true
scope: raw
tokens:
- '^1\. [^\n]*\n\n?( | | )[`\w-]'
- '^- [^\n]*\n\n?( | | )[`\w-]'
# Regex guide:
#
# "^1. [^\n]*" - Lines that start with an ordered list.
# "^- [^\n]*" - Lines that start with an unordered list.
#
# "\n\n?" - Then one or two newlines
#
# Ordered lists: "( | | )" - One, two, or four spaces (three = OK)
# Unordered lists: "( | | )" - One, three, or four spaces (two = OK)
#
# "[`\w-]" - Any one of:
#
# - A backtick - For code blocks (after a list item).
# - A letter/number - For alert boxes, sentences, and nested ordered lists (after a list item).
# - A hyphen - For nested unordered lists (after a list item).

View File

@ -43,17 +43,17 @@ To set up your self-hosted model deployment infrastructure:
Install one of the following GitLab-approved LLM models:
| Model | Code Completion | Code Generation | Duo Chat |
|-----------------------------------------------------------------------------------------------------------------------------|-----------------|-----------------|----------|
| [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | ✅ | - | - |
| [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) (Instruction) | - | ✅ | - |
| [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) (Code) | ✅ | - | - |
| [Code-Llama 13b-code](https://huggingface.co/meta-llama/CodeLlama-13b-hf) | ✅ | - | - |
| [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | - | ✅ | - |
| [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) (see [setup instructions](litellm_proxy_setup.md#example-setup-for-codestral-with-ollama)) | ✅ | ✅ | - |
| [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | - | ✅ | ✅ |
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | - | ✅ | ✅ |
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | - | ✅ | ✅ |
| Model | Code completion | Code generation | GitLab Duo Chat |
|------------------------------------------------------------------------------------|-----------------|-----------------|---------|
| [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) (Instruction) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) (Code) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [Code-Llama 13b-code](https://huggingface.co/meta-llama/CodeLlama-13b-hf) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) (see [setup instructions](litellm_proxy_setup.md#example-setup-for-codestral-with-ollama)) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
### Recommended serving architectures

View File

@ -32,9 +32,9 @@ When Terraform state administration is disabled:
- On the left sidebar, you cannot select **Operate > Terraform states**.
- Any CI/CD jobs that access the Terraform state fail with this error:
```shell
Error refreshing state: HTTP remote state endpoint invalid auth
```
```shell
Error refreshing state: HTTP remote state endpoint invalid auth
```
To disable Terraform administration, follow the steps below according to your installation.

View File

@ -34889,18 +34889,6 @@ Represents a generic widget definition.
| ---- | ---- | ----------- |
| <a id="workitemwidgetdefinitiongenerictype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetDefinitionHealthStatus`
Represents a health status widget definition.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetdefinitionhealthstatuseditable"></a>`editable` | [`Boolean!`](#boolean) | Indicates whether editable health status is available. |
| <a id="workitemwidgetdefinitionhealthstatusrollup"></a>`rollUp` | [`Boolean!`](#boolean) | Indicates whether rolled up health status is available. |
| <a id="workitemwidgetdefinitionhealthstatustype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetDefinitionHierarchy`
Represents a hierarchy widget definition.
@ -34998,7 +34986,6 @@ Represents a health status widget.
| <a id="workitemwidgethealthstatushealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Health status of the work item. |
| <a id="workitemwidgethealthstatusrolleduphealthstatus"></a>`rolledUpHealthStatus` **{warning-solid}** | [`[WorkItemWidgetHealthStatusCount!]`](#workitemwidgethealthstatuscount) | **Introduced** in GitLab 17.3. **Status**: Experiment. Rolled up health status of the work item. |
| <a id="workitemwidgethealthstatustype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
| <a id="workitemwidgethealthstatuswidgetdefinition"></a>`widgetDefinition` | [`WorkItemWidgetDefinitionHealthStatus`](#workitemwidgetdefinitionhealthstatus) | Health status widget definition. |
### `WorkItemWidgetHealthStatusCount`
@ -40735,7 +40722,6 @@ Implementations:
- [`WorkItemWidgetDefinitionAssignees`](#workitemwidgetdefinitionassignees)
- [`WorkItemWidgetDefinitionGeneric`](#workitemwidgetdefinitiongeneric)
- [`WorkItemWidgetDefinitionHealthStatus`](#workitemwidgetdefinitionhealthstatus)
- [`WorkItemWidgetDefinitionHierarchy`](#workitemwidgetdefinitionhierarchy)
- [`WorkItemWidgetDefinitionLabels`](#workitemwidgetdefinitionlabels)
- [`WorkItemWidgetDefinitionWeight`](#workitemwidgetdefinitionweight)

View File

@ -102,11 +102,11 @@ plan:
key: TP
name: test plan
stages:
- Default Stage:
manual: false
final: false
jobs:
- Default Job
- Default Stage:
manual: false
final: false
jobs:
- Default Job
Default Job:
key: JOB1
tasks:
@ -116,20 +116,20 @@ Default Job:
- script:
interpreter: SHELL
scripts:
- |-
ruby -v # Print out ruby version for debugging
bundle config set --local deployment true # Install dependencies into ./vendor/ruby
bundle install -j $(nproc)
rubocop
rspec spec
- |-
ruby -v # Print out ruby version for debugging
bundle config set --local deployment true # Install dependencies into ./vendor/ruby
bundle install -j $(nproc)
rubocop
rspec spec
description: run bundler
artifact-subscriptions: []
repositories:
- Demo Project:
scope: global
- Demo Project:
scope: global
triggers:
- polling:
period: '180'
- polling:
period: '180'
branches:
create: manually
delete: never
@ -150,20 +150,20 @@ version: 2
plan:
key: AB-TP
plan-permissions:
- users:
- root
permissions:
- view
- edit
- build
- clone
- admin
- view-configuration
- roles:
- logged-in
- anonymous
permissions:
- view
- users:
- root
permissions:
- view
- edit
- build
- clone
- admin
- view-configuration
- roles:
- logged-in
- anonymous
permissions:
- view
...
```
@ -175,7 +175,7 @@ default:
image: ruby:latest
stages:
- default-stage
- default-stage
job1:
stage: default-stage
@ -416,10 +416,10 @@ Default Job:
- script:
interpreter: SHELL
scripts:
- |-
ruby -v
bundle config set --local deployment true
bundle install -j $(nproc)
- |-
ruby -v
bundle config set --local deployment true
bundle install -j $(nproc)
description: run bundler
other:
concurrent-build-plugin: system-default
@ -489,8 +489,8 @@ For example, in a Bamboo build plan:
version: 2
#...
triggers:
- polling:
period: '180'
- polling:
period: '180'
```
GitLab CI/CD pipelines can be triggered based on code change, on schedule, or triggered by

View File

@ -169,6 +169,7 @@ to stop them from running:
- Use [`rules`](../yaml/index.md#rules) to skip tests that aren't needed. For example,
skip backend tests when only the frontend code is changed.
- Run non-essential [scheduled pipelines](schedules.md) less frequently.
- Distribute [`cron` schedules](schedules.md#view-and-optimize-pipeline-schedules) evenly across time.
### Fail fast

View File

@ -96,7 +96,8 @@ When working with pipeline schedules, you might encounter the following issues.
### Short refs are expanded to full refs
When you provide a short `ref` to the API, it is automatically expanded to a full `ref`. This behavior is intended and ensures explicit resource identification.
When you provide a short `ref` to the API, it is automatically expanded to a full `ref`.
This behavior is intended and ensures explicit resource identification.
The API accepts both short refs (such as `main`) and full refs (such as `refs/heads/main` or `refs/tags/main`).
@ -108,3 +109,31 @@ In some cases, the API can't automatically expand a short `ref` to a full `ref`.
- You provide a short `ref`, but no branch or tag with that name exists.
To resolve this issue, provide the full `ref` to ensure the correct resource is identified.
### View and optimize pipeline schedules
To prevent [excessive load](pipeline_efficiency.md) caused by too many pipelines starting simultaneously,
you can review and optimize your pipeline schedules.
To get an overview of all existing schedules and identify opportunities to distribute them more evenly:
1. Run this command to extract and format schedule data:
```shell
outfile=/tmp/gitlab_ci_schedules.tsv
sudo gitlab-psql --command "
COPY (SELECT
ci_pipeline_schedules.cron,
projects.path AS project,
users.email
FROM ci_pipeline_schedules
JOIN projects ON projects.id = ci_pipeline_schedules.project_id
JOIN users ON users.id = ci_pipeline_schedules.owner_id
) TO '$outfile' CSV HEADER DELIMITER E'\t' ;"
sort "$outfile" | uniq -c | sort -n
```
1. Review the output to identify popular `cron` patterns.
For example, you might see many schedules set to run at the start of each hour (`0 * * * *`).
1. Adjust the schedules to create a staggered [`cron` pattern](../../topics/cron/index.md#cron-syntax), especially for large repositories.
For example, instead of multiple schedules running at the start of each hour, distribute them throughout the hour (`5 * * * *`, `15 * * * *`, `25 * * * *`).

View File

@ -127,18 +127,18 @@ GitLab also supports the following user types for unique use cases:
- [Auditor Users](../administration/auditor_users.md) - The auditor role provides read-only access to all groups, projects and other resources except for the **Admin** area and project/group settings. You can use the auditor role when engaging with third-party auditors that require access to certain projects to validate processes.
- [External Users](../administration/external_users.md) -
External users can be set to provide limited access for users that
may not be part of the organization. Typically, this can be used to
satisfy managing access for contractors or other third parties.
Controls such as IA-4(4) require non-organizational users to be
identified and managed in accordance with company policy. Setting
external users can reduce risk to an organization by limiting access
to projects by default and assisting administrators in identifying
which users are not employed by the organization.
External users can be set to provide limited access for users that
may not be part of the organization. Typically, this can be used to
satisfy managing access for contractors or other third parties.
Controls such as IA-4(4) require non-organizational users to be
identified and managed in accordance with company policy. Setting
external users can reduce risk to an organization by limiting access
to projects by default and assisting administrators in identifying
which users are not employed by the organization.
- [Service Accounts](../user/profile/service_accounts.md#self-managed-gitlab) -
Service accounts may be added to accommodate automated tasks.
Service accounts do not use a seat under the license.
Service accounts may be added to accommodate automated tasks.
Service accounts do not use a seat under the license.
**Admin** area - In the **Admin** area, administrators can [export permissions](../administration/admin_area.md#user-permission-export),
[review user identities](../administration/admin_area.md#user-identities), [administer groups](../administration/admin_area.md#administering-groups),
@ -148,60 +148,60 @@ requirements:
- [Reset user password](reset_user_password.md) when suspected of compromise.
- [Unlock users](unlock_user.md).
By default, GitLab locks users after 10 failed sign-in attempts.
Users remain locked for 10 minutes or until an administrator unlocks
the user. In GitLab 16.5 and later, administrators can [use the API](../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)
to configure max login attempts and time period for remaining locked
out. Per guidance in AC-7, FedRAMP defers to NIST 800-63B for
defining parameters for account lockouts, which the default setting
satisfies.
By default, GitLab locks users after 10 failed sign-in attempts.
Users remain locked for 10 minutes or until an administrator unlocks
the user. In GitLab 16.5 and later, administrators can [use the API](../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)
to configure max login attempts and time period for remaining locked
out. Per guidance in AC-7, FedRAMP defers to NIST 800-63B for
defining parameters for account lockouts, which the default setting
satisfies.
- Review [abuse reports](../administration/review_abuse_reports.md)
or [spam logs](../administration/review_spam_logs.md).
FedRAMP requires organizations to monitor accounts for atypical use
(AC-2(12)). GitLab empowers users to flag abuse in abuse reports,
where administrators can remove access pending investigation. Spam
logs are consolidated in the **Spam logs** section of the **Admin** area.
Administrators can remove, block, or trust users flagged in that
area.
or [spam logs](../administration/review_spam_logs.md).
FedRAMP requires organizations to monitor accounts for atypical use
(AC-2(12)). GitLab empowers users to flag abuse in abuse reports,
where administrators can remove access pending investigation. Spam
logs are consolidated in the **Spam logs** section of the **Admin** area.
Administrators can remove, block, or trust users flagged in that
area.
- [Set password storage parameters](password_storage.md).
Stored secrets must satisfy FIPS 140-2 or 140-3 as outlined in
SC-13. PBKDF2+SHA512 is supported with FIPS compliant ciphers when
FIPS mode is enabled.
Stored secrets must satisfy FIPS 140-2 or 140-3 as outlined in
SC-13. PBKDF2+SHA512 is supported with FIPS compliant ciphers when
FIPS mode is enabled.
- [Credentials inventory](../administration/credentials_inventory.md)
enables administrators to review all secrets used in a GitLab
self-managed instance in one place. A consolidated view of
credentials, tokens, and keys may assist with satisfying
requirements such as reviewing passwords or rotating credentials.
enables administrators to review all secrets used in a GitLab
self-managed instance in one place. A consolidated view of
credentials, tokens, and keys may assist with satisfying
requirements such as reviewing passwords or rotating credentials.
- [Set customer password length limits](password_length_limits.md).
FedRAMP defers to NIST 800-63B in IA-5 for establishing password
length requirements. GitLab supports 8-128 character passwords, with
8 characters set as the default. GitLab provides [instructions for updating the minimum password length](password_length_limits.md#modify-minimum-password-length)
with the GitLab UI, which organizations interested
in enforcing longer passwords can use. Additionally, self-managed customers
may [configure complexity requirements](../administration/settings/sign_up_restrictions.md#password-complexity-requirements)
through the **Admin** area UI.
FedRAMP defers to NIST 800-63B in IA-5 for establishing password
length requirements. GitLab supports 8-128 character passwords, with
8 characters set as the default. GitLab provides [instructions for updating the minimum password length](password_length_limits.md#modify-minimum-password-length)
with the GitLab UI, which organizations interested
in enforcing longer passwords can use. Additionally, self-managed customers
may [configure complexity requirements](../administration/settings/sign_up_restrictions.md#password-complexity-requirements)
through the **Admin** area UI.
- [Default session durations](../administration/settings/account_and_limit_settings.md#customize-the-default-session-duration) -
FedRAMP establishes that users that have been inactive for a set
time period should be logged out. FedRAMP does not specify the time
period, however, clarifies that for privileged users they should be
logged out at the end of the standard work period. Administrators
can establish [default session durations](../administration/settings/account_and_limit_settings.md#customize-the-default-session-duration).
FedRAMP establishes that users that have been inactive for a set
time period should be logged out. FedRAMP does not specify the time
period, however, clarifies that for privileged users they should be
logged out at the end of the standard work period. Administrators
can establish [default session durations](../administration/settings/account_and_limit_settings.md#customize-the-default-session-duration).
- [Provisioning New Users](../user/profile/account/create_accounts.md) -
Administrators can create new users for their GitLab account with the
**Admin** area UI. In compliance with IA-5, GitLab requires new users to
change their passwords on first login.
Administrators can create new users for their GitLab account with the
**Admin** area UI. In compliance with IA-5, GitLab requires new users to
change their passwords on first login.
- Deprovisioning Users - Administrators are able to [remove users with the **Admin** area UI](../user/profile/account/delete_account.md#delete-users-and-user-contributions).
An alternative to deleting users is to [block a user](../administration/moderate_users.md#block-a-user)
and remove all access. Blocking a user maintains their data in
repositories while removing all access. Blocked users do not impact
seat counts.
An alternative to deleting users is to [block a user](../administration/moderate_users.md#block-a-user)
and remove all access. Blocking a user maintains their data in
repositories while removing all access. Blocked users do not impact
seat counts.
- Deactivate Users - Inactive users that have been identified during account reviews [may be temporarily deactivated](../administration/moderate_users.md#deactivate-a-user). Deactivation is similar to blocking, but there are a few important differences. Deactivating a user does not prohibit the user from signing into the GitLab UI. A deactivated user can become active again by signing in. A deactivated user:
- Cannot access repositories or the API.
@ -396,19 +396,19 @@ roles after the necessary reviews have been completed. Additional merge
settings to consider:
- Remove all approvals when a commit is added - Ensures that approvals
are not carried over when new commits are made to a merge request.
are not carried over when new commits are made to a merge request.
- Restrict individuals who can dismiss code change reviews.
- Assign [code owners](../user/project/codeowners/index.md#codeowners-file)
to be notified when sensitive code or configurations are changed through
merge requests.
to be notified when sensitive code or configurations are changed through
merge requests.
- [Ensure all open comments are resolved before allowing code change merging](../user/project/merge_requests/index.md#prevent-merge-unless-all-threads-are-resolved).
- [Configure push rules](../user/project/repository/push_rules.md) -
Push rules can be configured to meet requirements such as reviewing
signed code, verifying users, and more.
Push rules can be configured to meet requirements such as reviewing
signed code, verifying users, and more.
**Testing and Validation of Changes**
@ -509,41 +509,41 @@ to ensure that Geo is configured appropriately for each use case.
Implementing Geo provides the following benefits:
- Reduce from minutes to seconds the time taken for distributed
developers to clone and fetch large repositories and projects.
developers to clone and fetch large repositories and projects.
- Enable developers to contribute ideas and work in parallel, across
regions..
regions.
- Balance the read-only load between primary and secondary sites.
- Can be used for cloning and fetching projects, in addition to
reading any data available in the GitLab web interface (see
limitations).
reading any data available in the GitLab web interface (see
limitations).
- Overcomes slow connections between distant offices, saving time by
improving speed for distributed teams.
improving speed for distributed teams.
- Helps reduce the loading time for automated tasks, custom
integrations, and internal workflows.
integrations, and internal workflows.
- Can quickly fail over to a secondary site in a disaster recovery
scenario.
scenario.
- Allows planned failover to a secondary site.
Geo provides the following core features:
- Read-only secondary sites: Maintain one primary GitLab site while
still enabling read-only secondary sites for distributed teams.
still enabling read-only secondary sites for distributed teams.
- Authentication system hooks: Secondary sites receive all
authentication data (like user accounts and logins) from the primary
instance.
authentication data (like user accounts and logins) from the primary
instance.
- An intuitive UI: Secondary sites use the same web interface as the
primary site. In addition, there are visual notifications that block
write operations and make it clear that a user is in a secondary
site.
primary site. In addition, there are visual notifications that block
write operations and make it clear that a user is in a secondary
site.
Additional Geo Resources:

View File

@ -105,7 +105,7 @@ Prerequisites:
- You have installed and enabled the
[GitLab Workflow extension for VS Code](../../../../editor_extensions/visual_studio_code/index.md).
- You have completed the [VS Code extension setup](https://gitlab.com/gitlab-org/gitlab-vscode-extension/#setup)
instructions, and authorized the extension to access your GitLab account.
instructions, and authorized the extension to access your GitLab account.
To do this:
@ -129,7 +129,7 @@ Prerequisites:
- You have installed and enabled the
[GitLab plugin for JetBrains IDEs](../../../../editor_extensions/jetbrains_ide/index.md).
- You have completed the [Jetbrains extension setup](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin#setup)
instructions, and authorized the extension to access your GitLab account.
instructions, and authorized the extension to access your GitLab account.
To do this:

View File

@ -38,15 +38,10 @@ If you are a self-managed user, ensure that Code Suggestions for the [GitLab Web
If the settings are enabled, but suggestions are still not displayed, try the following steps:
1. In the GitLab Workflow **Extension Settings**, enable **GitLab: Debug**.
1. Open the extension log in **View > Output** and change the dropdown list to **GitLab Workflow** as the log filter. The command palette command is `GitLab: Show Extension Logs`.
1. Disable and re-enable the **Enable code completion** checkbox.
1. Verify that the debug log contains similar output:
```shell
2023-07-14T17:29:00:763 [debug]: Disabling code completion
2023-07-14T17:29:01:802 [debug]: Enabling code completion
2023-07-14T17:29:01:802 [debug]: AI Assist: Using server: https://cloud.gitlab.com/ai/v2/code/completions
```
1. On the top menu, select **View > Output** to open the bottom panel, then either:
- In the command palette, select `GitLab: Show Extension Logs`.
- In the bottom panel, on the right, select the dropdown list to filter the logs. Select **GitLab Workflow**.
1. In the GitLab Workflow **Extension Settings**, clear and re-select the **GitLab Duo Code Suggestions** checkbox.
To enable debug logging for the Language Server (LS), enable **GitLab Ls: Debug**.

View File

@ -87,11 +87,12 @@ For more information, see [Pack-objects cache](../../../../administration/gitaly
### Reduce concurrent clones in CI/CD
CI/CD loads tend to be concurrent because pipelines are scheduled during set times.
CI/CD loads tend to be concurrent because pipelines are [scheduled during set times](../../../../ci/pipelines/pipeline_efficiency.md#reduce-how-often-jobs-run).
As a result, the Git requests against the repositories can spike notably during
these times and lead to reduced performance for both CI/CD and users alike.
Reduce CI/CD pipeline concurrency by staggering them to run at different times.
Reduce CI/CD pipeline concurrency by [staggering them](../../../../ci/pipelines/schedules.md#view-and-optimize-pipeline-schedules)
to run at different times.
For example, a set running at one time and another set running several minutes
later.

View File

@ -15625,6 +15625,9 @@ msgstr ""
msgid "Copy issue URL to clipboard"
msgstr ""
msgid "Copy item URL"
msgstr ""
msgid "Copy key"
msgstr ""
@ -37712,9 +37715,6 @@ msgstr ""
msgid "Open evidence JSON in new tab"
msgstr ""
msgid "Open full view"
msgstr ""
msgid "Open in Gitpod"
msgstr ""
@ -37724,6 +37724,9 @@ msgstr ""
msgid "Open in file view"
msgstr ""
msgid "Open in full page"
msgstr ""
msgid "Open in your IDE"
msgstr ""

View File

@ -249,7 +249,7 @@
"yaml": "^2.0.0-10"
},
"devDependencies": {
"@gitlab/eslint-plugin": "20.2.0",
"@gitlab/eslint-plugin": "20.2.1",
"@gitlab/stylelint-config": "6.2.1",
"@graphql-eslint/eslint-plugin": "3.20.1",
"@originjs/vite-plugin-commonjs": "^1.0.3",

View File

@ -97,22 +97,6 @@ tests = [
expected: ['spec/initializers/action_mailer_hooks_spec.rb']
},
{
explanation: 'FOSS Snowplow definitions map to event validation spec',
changed_file: 'config/events/status_page_incident_unpublished.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'EE Snowplow definitions map to event validation spec',
changed_file: 'ee/config/events/licenses_list_viewed.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'Snowplow schema maps to event validation spec',
changed_file: 'config/events/schema.json',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'DB structure should map to schema spec',
changed_file: 'db/structure.sql',
@ -255,7 +239,101 @@ tests = [
# Note: The metrics seem to be changed every year or so, so this test will fail once a year or so.
# You will need to change the metric below for another metric present in the project.
changed_file: 'ee/config/metrics/counts_all/20221114065035_delete_merge_request.yml',
expected: ['ee/spec/config/metrics/every_metric_definition_spec.rb']
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
]
},
{
explanation: 'FOSS metric defintions map to metric validation specs',
changed_file: 'config/metrics/counts_28d/count_total_merge_request_click_start_review_on_overview_tab_monthly.yml',
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
]
},
{
explanation: 'Metric schema maps to metric validation spec',
changed_file: 'config/metrics/schema/base.json',
expected: %w[
spec/config/metrics/every_metric_definition_spec.rb
ee/spec/config/metrics/every_metric_definition_spec.rb
spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'FOSS Snowplow definitions map to event validation spec',
changed_file: 'config/events/status_page_incident_unpublished.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'EE Snowplow definitions map to event validation spec',
changed_file: 'ee/config/events/licenses_list_viewed.yml',
expected: ['spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb']
},
{
explanation: 'Snowplow schema maps to event validation spec',
changed_file: 'config/events/schema.json',
expected: %w[
spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events application logic maps to internal events tooling',
changed_file: 'lib/gitlab/internal_events.rb',
expected: %w[
spec/lib/gitlab/internal_events_spec.rb
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events CLI entrypoint maps to internal events tooling',
changed_file: 'scripts/internal_events/cli.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events CLI logic maps to internal events tooling',
changed_file: 'scripts/internal_events/cli/global_state.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events default test setup maps to internal events tooling',
changed_file: 'spec/support/shared_examples/controllers/internal_event_tracking_examples.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events custom test setup maps to internal events tooling',
changed_file: 'spec/support/matchers/internal_events_matchers.rb',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{
explanation: 'Internal Events test cases map to internal events tooling',
changed_file: 'spec/fixtures/scripts/internal_events/new_events.yml',
expected: %w[
spec/scripts/internal_events/cli_spec.rb
spec/support_specs/matchers/internal_events_matchers_spec.rb
]
},
{

View File

@ -1126,9 +1126,10 @@ describe('CE IssuesListApp component', () => {
});
it('selects active issuable', () => {
expect(findIssuableList().props('activeIssuable')).toEqual(
getIssuesQueryResponse.data.project.issues.nodes[0],
);
expect(findIssuableList().props('activeIssuable')).toEqual({
...getIssuesQueryResponse.data.project.issues.nodes[0],
fullPath: defaultProvide.fullPath,
});
});
describe('when closing the drawer', () => {

View File

@ -36,7 +36,7 @@ exports[`Merge request dashboard assigned users component renders user avatars 1
>
<img
alt="avatar"
class="gl-avatar gl-avatar-circle gl-avatar-s32"
class="!gl-bg-white gl-avatar gl-avatar-circle gl-avatar-s32"
src="/root"
/>
<span
@ -67,7 +67,7 @@ exports[`Merge request dashboard assigned users component renders user avatars 1
>
<img
alt="avatar"
class="gl-avatar gl-avatar-circle gl-avatar-s32"
class="!gl-bg-white gl-avatar gl-avatar-circle gl-avatar-s32"
src="/root"
/>
<span

View File

@ -0,0 +1,72 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NotePreview from '~/notes/components/note_preview.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import noteQuery from '~/notes/graphql/note.query.graphql';
const noteQueryHandler = jest.fn().mockResolvedValue({
data: {
note: {
id: 'gid://gitlab/Note/1',
author: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
avatar_url: '',
web_url: '',
web_path: '',
},
body_html: 'my quick note',
created_at: '2020-01-01T10:00:00.000Z',
last_edited_at: null,
last_edited_by: null,
internal: false,
url: '/note/1',
},
},
});
describe('Note preview', () => {
let wrapper;
Vue.use(VueApollo);
const createComponent = ({ noteId = '1' }) => {
wrapper = shallowMount(NotePreview, {
apolloProvider: createMockApollo([[noteQuery, noteQueryHandler]]),
propsData: {
noteId,
},
});
};
const findNoteableNote = () => wrapper.findComponent(NoteableNote);
it('does nothing if URL does not contain a note id', () => {
createComponent({ noteId: null });
expect(noteQueryHandler).not.toHaveBeenCalled();
expect(wrapper.html()).toBe('');
});
it('does nothing if URL links to a system note', () => {
createComponent({
noteId: '50f036b11addf3c1dc3d4b43a96cfeb799ae2f7c',
});
expect(noteQueryHandler).not.toHaveBeenCalled();
expect(wrapper.html()).toBe('');
});
it('renders a note', async () => {
createComponent({ noteId: '1234' });
await waitForPromises();
expect(findNoteableNote().exists()).toBe(true);
expect(findNoteableNote().props('showReplyButton')).toBe(false);
});
});

View File

@ -15,6 +15,7 @@ import notesEventHub from '~/notes/event_hub';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import NotePreview from '~/notes/components/note_preview.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@ -22,6 +23,9 @@ import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import * as mockData from '../mock_data';
jest.mock('~/behaviors/markdown/render_gfm');
jest.mock('~/lib/utils/resize_observer', () => ({
scrollToTargetOnResize: jest.fn(),
}));
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
@ -316,13 +320,13 @@ describe('note_app', () => {
return waitForPromises();
});
it('should listen hashchange event', () => {
const hash = 'some dummy hash';
it('should listen hashchange event for notes', () => {
const hash = 'note_1234';
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(hash);
const dispatchMock = jest.spyOn(store, 'dispatch');
window.dispatchEvent(new Event('hashchange'), hash);
expect(dispatchMock).toHaveBeenCalledWith('setTargetNoteHash', 'some dummy hash');
expect(dispatchMock).toHaveBeenCalledWith('setTargetNoteHash', 'note_1234');
});
});
@ -375,6 +379,30 @@ describe('note_app', () => {
});
});
describe('preview note shown inside skeleton notes', () => {
it.each`
urlHash | exists
${''} | ${false}
${'note_123'} | ${true}
`('url is `$urlHash`', ({ urlHash, exists }) => {
jest.spyOn(urlUtility, 'getLocationHash').mockReturnValue(urlHash);
store = createStore();
store.state.isLoading = true;
store.state.targetNoteHash = urlHash;
wrapper = shallowMount(NotesApp, {
propsData,
store,
stubs: {
'ordered-layout': OrderedLayout,
},
});
expect(wrapper.findComponent(NotePreview).exists()).toBe(exists);
});
});
describe('when multiple draft types are present', () => {
beforeEach(() => {
store = createStore();

View File

@ -45,7 +45,6 @@ describe('Work item add note', () => {
workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
isGroup = false,
workItemType = 'Task',
isInternalThread = false,
isNewDiscussion = false,
@ -66,9 +65,6 @@ describe('Work item add note', () => {
[workItemByIidQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
]),
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemId: id,

View File

@ -48,15 +48,11 @@ describe('Work item comment form component', () => {
isNewDiscussion = false,
workItemState = STATE_OPEN,
workItemType = 'Task',
isGroup = false,
hasReplies = false,
isDiscussionResolved = false,
isDiscussionResolvable = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemCommentForm, {
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemIid: '1',

View File

@ -90,7 +90,6 @@ describe('Work Item Note', () => {
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
isGroup = false,
updateNoteMutationHandler = successHandler,
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
@ -98,9 +97,6 @@ describe('Work Item Note', () => {
workItemByIidResponseHandler = workItemResponseHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
provide: {
isGroup,
},
propsData: {
fullPath: 'test-project-path',
workItemId,
@ -155,7 +151,7 @@ describe('Work Item Note', () => {
});
it('should show the awards list when in edit mode', async () => {
createComponent({ note: mockWorkItemCommentNote, workItemsAlpha: true });
createComponent({ note: mockWorkItemCommentNote });
findNoteActions().vm.$emit('startEditing');
await nextTick();
expect(findAwardsList().exists()).toBe(true);
@ -438,7 +434,7 @@ describe('Work Item Note', () => {
});
it('passes note props to awards list', () => {
createComponent({ note: mockWorkItemCommentNote, workItemsAlpha: true });
createComponent({ note: mockWorkItemCommentNote });
expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote);
expect(findAwardsList().props('workItemIid')).toBe('1');

View File

@ -148,10 +148,6 @@ describe('WorkItemActions component', () => {
hideSubscribe,
hasChildren,
},
provide: {
isGroup: false,
glFeatures: { workItemsBeta: true, workItemsAlpha: true },
},
mocks: {
$toast,
},

View File

@ -55,7 +55,6 @@ describe('WorkItemAttributesWrapper component', () => {
const createComponent = ({
workItem = workItemQueryResponse.data.workItem,
workItemsBeta = false,
workItemsAlpha = false,
groupPath = '',
workItemParticipantsQueryHandler = workItemParticipantsQuerySuccessHandler,
@ -72,7 +71,6 @@ describe('WorkItemAttributesWrapper component', () => {
provide: {
hasSubepicsFeature: true,
glFeatures: {
workItemsBeta,
workItemsAlpha,
},
},

View File

@ -32,7 +32,6 @@ describe('WorkItemCreatedUpdated component', () => {
confidential = false,
discussionLocked = false,
updateInProgress = false,
isGroup = false,
} = {}) => {
const workItemQueryResponse = workItemByIidResponseFactory({
author,
@ -45,9 +44,6 @@ describe('WorkItemCreatedUpdated component', () => {
wrapper = shallowMount(WorkItemCreatedUpdated, {
apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
provide: {
isGroup,
},
propsData: {
fullPath: '/some/project',
workItemIid,

View File

@ -29,7 +29,6 @@ describe('WorkItemDescriptionRendered', () => {
workItemDescription = defaultWorkItemDescription,
canEdit = false,
mockComputed = {},
hasWorkItemsBeta = false,
} = {}) => {
wrapper = shallowMount(WorkItemDescriptionRendered, {
propsData: {
@ -40,7 +39,6 @@ describe('WorkItemDescriptionRendered', () => {
computed: mockComputed,
provide: {
fullPath: 'full/path',
workItemsBeta: hasWorkItemsBeta,
},
});
};

View File

@ -109,12 +109,10 @@ describe('WorkItemDetail component', () => {
mutationHandler,
error = undefined,
workItemsAlphaEnabled = false,
workItemsBeta = false,
namespaceLevelWorkItems = true,
hasSubepicsFeature = true,
router = true,
modalIsGroup = null,
isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo([
@ -141,14 +139,12 @@ describe('WorkItemDetail component', () => {
provide: {
glFeatures: {
workItemsAlpha: workItemsAlphaEnabled,
workItemsBeta,
namespaceLevelWorkItems,
},
hasSubepicsFeature,
fullPath: 'group/project',
groupPath: 'group',
reportAbusePath: '/report/abuse/path',
isGroup,
},
stubs: {
WorkItemAncestors: true,

View File

@ -132,7 +132,6 @@ describe('WorkItemDevelopment CE', () => {
.mockResolvedValue(closedWorkItemWithAutoCloseFlagEnabled);
const createComponent = ({
isGroup = false,
workItemId = 'gid://gitlab/WorkItem/1',
workItemIid = '1',
workItemFullPath = 'full-path',
@ -153,7 +152,6 @@ describe('WorkItemDevelopment CE', () => {
workItemFullPath,
},
provide: {
isGroup,
glFeatures: {
workItemsAlpha: workItemsAlphaEnabled,
},

View File

@ -1,5 +1,4 @@
import { GlDrawer, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@ -10,6 +9,7 @@ import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
Vue.use(VueApollo);
@ -27,10 +27,10 @@ describe('WorkItemDrawer', () => {
const createComponent = ({
open = false,
activeItem = { iid: '1', webUrl: 'test' },
activeItem = { iid: '1', webUrl: 'test', fullPath: 'gitlab-org/gitlab' },
issuableType = TYPE_ISSUE,
} = {}) => {
wrapper = shallowMount(WorkItemDrawer, {
wrapper = shallowMountExtended(WorkItemDrawer, {
propsData: {
activeItem,
open,
@ -44,7 +44,6 @@ describe('WorkItemDrawer', () => {
reportAbusePath: '',
groupPath: '',
hasSubepicsFeature: false,
isGroup: false,
},
apolloProvider: createMockApollo([[deleteWorkItemMutation, deleteWorkItemMutationHandler]]),
});
@ -56,10 +55,26 @@ describe('WorkItemDrawer', () => {
expect(findGlDrawer().props('open')).toBe(false);
});
it('displays correct URL in link', () => {
it('displays correct URL and text in link', () => {
createComponent();
expect(wrapper.findComponent(GlLink).attributes('href')).toBe('test');
const link = wrapper.findComponent(GlLink);
expect(link.attributes('href')).toBe('test');
expect(link.text()).toBe('gitlab#1');
});
it('displays the correct URL in the full page button', () => {
createComponent();
expect(wrapper.findByTestId('work-item-drawer-link-button').attributes('href')).toBe('test');
});
it('has a copy to clipboard button for the item URL', () => {
createComponent();
expect(
wrapper.findByTestId('work-item-drawer-copy-button').attributes('data-clipboard-text'),
).toBe('test');
});
it('emits `close` event when drawer is closed', () => {

View File

@ -87,9 +87,6 @@ describe('WorkItemChildrenWrapper', () => {
wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
apolloProvider: mockApollo,
provide: {
isGroup: false,
},
propsData: {
fullPath: 'test/project',
workItemType,

View File

@ -64,13 +64,9 @@ describe('WorkItemActions component', () => {
workItemType,
workItemReference,
},
provide: {
glFeatures: { workItemsBeta: true, workItemsAlpha: true },
},
mocks: {
$toast,
},
stubs: {},
});
};

View File

@ -49,7 +49,6 @@ describe('Work Item State toggle button component', () => {
workItemState = STATE_OPEN,
workItemType = 'Task',
hasComment = false,
isGroup = false,
} = {}) => {
wrapper = shallowMount(WorkItemStateToggle, {
apolloProvider: createMockApollo([
@ -57,9 +56,6 @@ describe('Work Item State toggle button component', () => {
[workItemByIidQuery, querySuccessHander],
[workItemLinkedItemsQuery, workItemLinkedItemsHandler],
]),
provide: {
isGroup,
},
propsData: {
workItemId: id,
workItemIid: iid,

View File

@ -31,7 +31,6 @@ describe('WorkItemTimeTracking component', () => {
},
provide: {
fullPath: 'gitlab-org/gitlab',
isGroup: false,
},
stubs: {
GlProgressBar: true,

View File

@ -84,9 +84,6 @@ describe('WorkItemTodo component', () => {
workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
provide: {
isGroup: false,
},
});
};

View File

@ -452,7 +452,7 @@ RSpec.describe Issuable, feature_category: :team_planning do
end
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
expect { issue.time_estimate = nil }.to change { issue.read_attribute(:time_estimate) }.to(nil)
expect { issue.time_estimate = 'invalid time' }.not_to raise_error
expect { issue.time_estimate = 22.33 }.not_to raise_error
end

View File

@ -2060,6 +2060,25 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
describe '#time_estimate' do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
context 'when time estimate on the issue record is NULL' do
before do
issue.update_column(:time_estimate, nil)
end
it 'sets time estimate to zeor on save' do
expect(issue.read_attribute(:time_estimate)).to be_nil
issue.save!
expect(issue.reload.read_attribute(:time_estimate)).to eq(0)
end
end
end
describe '#supports_move_and_clone?' do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:issue) { create(:incident, project: project) }

View File

@ -13,14 +13,14 @@ RSpec.describe ProjectStatistics do
describe 'scopes' do
describe '.for_project_ids' do
it 'returns only requested projects', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/448381' do
it 'returns only requested projects' do
stats = create_list(:project_statistics, 3)
project_ids = stats[0..1].map(&:project_id)
expected_ids = stats[0..1].map(&:id)
requested_stats = described_class.for_project_ids(project_ids).pluck(:id)
expect(requested_stats).to eq(expected_ids)
expect(requested_stats).to match_array(expected_ids)
end
end
end

View File

@ -472,8 +472,7 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
iid: { current: kind_of(Integer), previous: nil },
project_id: { current: project.id, previous: nil },
title: { current: opts[:title], previous: nil },
updated_at: { current: kind_of(Time), previous: nil },
time_estimate: { current: 0, previous: nil }
updated_at: { current: kind_of(Time), previous: nil }
},
object_attributes: include(
opts.merge(

View File

@ -12,7 +12,6 @@
- ee/spec/services/ee/users/create_service_spec.rb
- ee/spec/services/users/service_accounts/create_service_spec.rb
- ee/spec/services/epics/update_dates_service_spec.rb
- ee/spec/services/gitlab_subscriptions/trials/create_service_spec.rb
- spec/controllers/admin/groups_controller_spec.rb
- spec/controllers/admin/users_controller_spec.rb
- spec/controllers/groups_controller_spec.rb

View File

@ -49,12 +49,6 @@ mapping:
- source: 'config/initializers/(?<rest>.+)\.rb'
test: 'spec/initializers/%{rest}_spec.rb'
# Snowplow definitions map to schema validation spec
- source:
- '(?<prefix>ee/)?config/events/.+\.yml'
- 'config/events/schema\.json'
test: 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
# DB structure should map to schema spec
- source: 'db/structure\.sql'
test: 'spec/db/schema_spec.rb'
@ -136,13 +130,46 @@ mapping:
test:
- 'ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb'
# Any change to metrics definition should trigger the specs in the ee/spec/config/metrics/ folder.
#
# Note: We only have those tests for ee, even though we have non-ee metrics.
#
# Usage metric schema changes should trigger validations for all metrics and tooling
- source: 'config/metrics/schema/.*\.json'
test:
- 'spec/config/metrics/every_metric_definition_spec.rb'
- 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb'
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# Internal events schema changes should trigger validations for all events and tooling
- source: 'config/events/schema\.json'
test:
- 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# Any change to metric/event definitions should trigger the specs in the ee/spec/config/metrics/ folder.
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/287#note_1192008962
- source: 'ee/config/metrics/.*\.yml'
test: 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- source:
- '(ee/)?config/metrics/.*\.yml'
test:
- 'spec/config/metrics/every_metric_definition_spec.rb'
- 'ee/spec/config/metrics/every_metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_validate_all_spec.rb'
# Internal event/Snowplow event definitions map to schema validation spec
- source:
- '(ee/)?config/events/.+\.yml'
test: 'spec/lib/gitlab/tracking/event_definition_validator_validate_all_spec.rb'
# Changes to Internal Events behavior should trigger tests for the CLI and shared matchers
- source:
- 'lib/gitlab/internal_events\.rb'
- 'scripts/internal_events/cli(/.+)?\.rb'
- 'spec/support/shared_examples/controllers/internal_event_tracking_examples\.rb'
- 'spec/support/matchers/internal_events_matchers\.rb'
- 'spec/fixtures/scripts/internal_events/.+\.yml'
test:
- 'spec/scripts/internal_events/cli_spec.rb'
- 'spec/support_specs/matchers/internal_events_matchers_spec.rb'
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/146
- source: 'config/feature_categories\.yml'

View File

@ -1315,10 +1315,10 @@
core-js "^3.29.1"
mitt "^3.0.1"
"@gitlab/eslint-plugin@20.2.0":
version "20.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-20.2.0.tgz#12cefe534ef3204b800c4793ae5fbbd2d34f7d50"
integrity sha512-euERL6TPP9GrUCXJqZ3KvUsBdhh88pBtTxQMZa3RuRL+TwMBskwCZY+XQnXKKwiz55olZU6ZbUUN0NE9G4Ox0g==
"@gitlab/eslint-plugin@20.2.1":
version "20.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-20.2.1.tgz#2414f621ef3d88e90405436460febbfb247b4f91"
integrity sha512-n0ISWpRI/h6ZO8bfX10EY4u3y31Pdo9T2ieaRo1dSaC0nz7ZXHd23ykJ2gQDmYogjh6EHGS/SiqL1chMA4C+eQ==
dependencies:
"@typescript-eslint/eslint-plugin" "^7.14.1"
eslint-config-airbnb-base "^15.0.0"
@ -13104,7 +13104,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13157,7 +13166,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13171,6 +13180,13 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -14862,7 +14878,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -14880,6 +14896,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"