Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-04 12:13:07 +00:00
parent 8a0aaba672
commit 52fa21ddae
138 changed files with 1927 additions and 1122 deletions

View File

@ -5,13 +5,11 @@ import { mapGetters as mapVuexGetters } from 'vuex';
import { GlButton, GlTooltipDirective as GlTooltip, GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SET_REVIEW_BAR_RENDERED } from '~/batch_comments/stores/modules/batch_comments/mutation_types';
import { useBatchComments } from '~/batch_comments/store';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import SubmitDropdown from './submit_dropdown.vue';
import SubmitDrawer from './submit_drawer.vue';
export default {
components: {
@ -19,12 +17,10 @@ export default {
GlButton,
PreviewDropdown,
SubmitDropdown,
SubmitDrawer,
},
directives: {
GlTooltip,
},
mixins: [glFeatureFlagsMixin()],
data() {
return {
discarding: false,
@ -68,26 +64,24 @@ export default {
},
};
</script>
<template>
<nav class="review-bar-component js-review-bar" data-testid="review_bar_component">
<div class="review-bar-content gl-flex gl-justify-end" data-testid="review-bar-content">
<submit-drawer v-if="glFeatures.improvedReviewExperience" />
<template v-else>
<gl-button
v-gl-tooltip
icon="remove"
variant="danger"
category="tertiary"
class="gl-mr-3"
:title="__('Discard review')"
:aria-label="__('Discard review')"
:loading="discarding"
data-testid="discard-review-btn"
@click="showDiscardModal = true"
/>
<preview-dropdown />
<submit-dropdown />
</template>
<gl-button
v-gl-tooltip
icon="remove"
variant="danger"
category="tertiary"
class="gl-mr-3"
:title="__('Discard review')"
:aria-label="__('Discard review')"
:loading="discarding"
data-testid="discard-review-btn"
@click="showDiscardModal = true"
/>
<preview-dropdown />
<submit-dropdown />
</div>
<gl-modal
v-model="showDiscardModal"

View File

@ -0,0 +1,74 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions as mapVuexActions } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { GlDrawer } from '@gitlab/ui';
import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { useBatchComments } from '~/batch_comments/store';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
export default {
name: 'ReviewDrawer',
components: { GlDrawer, PreviewItem },
computed: {
...mapState(useBatchComments, ['sortedDrafts', 'draftsCount', 'drawerOpened']),
getDrawerHeaderHeight() {
if (!this.drawerOpened) return '0';
return getContentWrapperHeight();
},
},
methods: {
...mapVuexActions('diffs', ['goToFile']),
...mapActions(useBatchComments, ['scrollToDraft', 'setDrawerOpened']),
async onClickDraft(draft) {
if (this.viewDiffsFileByFile) {
await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
const url = new URL(setUrlParams({ commit_id: draft.position.head_sha }));
url.hash = `note_${draft.id}`;
visitUrl(url.toString());
} else {
await this.scrollToDraft(draft);
}
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<gl-drawer
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
:open="drawerOpened"
class="merge-request-review-drawer"
data-testid="review-drawer-toggle"
@close="setDrawerOpened(false)"
>
<template #title>
<h4 class="gl-m-0">{{ __('Submit your review') }}</h4>
</template>
<div>
<h5 class="h6 gl-mb-5 gl-mt-0" data-testid="reviewer-drawer-heading">
<template v-if="draftsCount > 0">
{{ n__('%d pending comment', '%d pending comments', draftsCount) }}
</template>
<template v-else>
{{ __('No pending comments') }}
</template>
</h5>
<preview-item
v-for="draft in sortedDrafts"
:key="draft.id"
:draft="draft"
class="gl-mb-3 gl-block"
@click="onClickDraft"
/>
</div>
</gl-drawer>
</template>

View File

@ -1,94 +0,0 @@
<script>
import { GlButton, GlDrawer } from '@gitlab/ui';
import { mapState, mapActions } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapActions as mapVuexActions } from 'vuex';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { useBatchComments } from '~/batch_comments/store';
import DraftsCount from './drafts_count.vue';
import PreviewItem from './preview_item.vue';
export default {
components: {
GlButton,
GlDrawer,
DraftsCount,
PreviewItem,
},
data() {
return {
open: false,
};
},
computed: {
...mapState(useBatchComments, ['sortedDrafts', 'draftsCount']),
getDrawerHeaderHeight() {
if (!this.open) return '0';
return getContentWrapperHeight();
},
},
methods: {
...mapVuexActions('diffs', ['goToFile']),
...mapActions(useBatchComments, ['scrollToDraft']),
async onClickDraft(draft) {
if (this.viewDiffsFileByFile) {
await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
const url = new URL(setUrlParams({ commit_id: draft.position.head_sha }));
url.hash = `note_${draft.id}`;
visitUrl(url.toString());
} else {
await this.scrollToDraft(draft);
}
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<div>
<gl-button variant="confirm" data-testid="review-drawer-toggle" @click="open = !open">
{{ __('Your review') }}
<drafts-count
v-if="draftsCount > 0"
variant="info"
data-testid="reviewer-drawer-drafts-count-badge"
/>
</gl-button>
<gl-drawer
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
:open="open"
class="merge-request-review-drawer"
data-testid="review-drawer-toggle"
@close="open = false"
>
<template #title>
<h4 class="gl-m-0">{{ __('Submit your review') }}</h4>
</template>
<div>
<h5 class="h6 gl-mb-5 gl-mt-0" data-testid="reviewer-drawer-heading">
<template v-if="draftsCount > 0">
{{ n__('%d pending comment', '%d pending comments', draftsCount) }}
</template>
<template v-else>
{{ __('No pending comments') }}
</template>
</h5>
<preview-item
v-for="draft in sortedDrafts"
:key="draft.id"
:draft="draft"
class="gl-mb-3 gl-block"
@click="onClickDraft"
/>
</div>
</gl-drawer>
</div>
</template>

View File

@ -0,0 +1,33 @@
<script>
import { GlButton } from '@gitlab/ui';
import { mapActions, mapState } from 'pinia';
import { useBatchComments } from '~/batch_comments/store';
import DraftsCount from './drafts_count.vue';
export default {
name: 'SubmitReviewButton',
components: {
GlButton,
DraftsCount,
},
computed: {
...mapState(useBatchComments, ['draftsCount', 'draftsCount', 'isReviewer']),
},
methods: {
...mapActions(useBatchComments, ['setDrawerOpened']),
},
};
</script>
<template>
<div v-if="draftsCount > 0 || isReviewer">
<gl-button variant="confirm" data-testid="review-drawer-toggle" @click="setDrawerOpened(true)">
{{ __('Your review') }}
<drafts-count
v-if="draftsCount > 0"
variant="info"
data-testid="reviewer-drawer-drafts-count-badge"
/>
</gl-button>
</div>
</template>

View File

@ -37,7 +37,7 @@ export const initReviewBar = () => {
...mapActions(useBatchComments, ['fetchDrafts']),
},
render(createElement) {
if (this.draftsCount === 0) return null;
if (window.gon?.features?.improvedReviewExperience || this.draftsCount === 0) return null;
return createElement('review-bar');
},

View File

@ -192,3 +192,7 @@ export function discardDrafts() {
}),
);
}
export function setDrawerOpened(opened) {
this.drawerOpened = opened;
}

View File

@ -19,6 +19,9 @@ export const useBatchComments = defineStore('batchComments', {
shouldAnimateReviewButton: false,
reviewBarRendered: false,
isMergeRequest: false,
drawerOpened: false,
// TODO: this gets populated from the sidebar_reviewers.vue, we should have a separate store for this
isReviewer: false,
};
},
actions: {

View File

@ -201,7 +201,7 @@ export default {
<slot name="action-buttons" :is-collapsible="isCollapsible"></slot>
<div v-if="isCollapsible" class="gl-border-l gl-ml-3 gl-pl-3">
<div v-if="isCollapsible" class="gl-border-l gl-ml-3 gl-border-l-section gl-pl-3">
<gl-button
data-testid="report-section-expand-button"
category="tertiary"

View File

@ -1,8 +1,7 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState } from 'vuex';
import { mapState } from 'pinia';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { EVT_EXPAND_ALL_FILES } from '../constants';
import eventHub from '../event_hub';
@ -24,7 +23,7 @@ export default {
};
},
computed: {
...mapState('diffs', ['diffFiles']),
...mapState(useLegacyDiffs, ['diffFiles']),
shouldDisplay() {
return !this.isDismissed && this.diffFiles.length > 1;
},

View File

@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective, GlIcon, GlLink, GlButtonGroup, GlButton, GlSprintf } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { __ } from '~/locale';
import { setUrlParams } from '~/lib/utils/url_utility';
import {
@ -12,6 +11,7 @@ import {
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
import { sanitize } from '~/lib/dompurify';
import FileBrowserToggle from '~/diffs/components/file_browser_toggle.vue';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
export default {
@ -35,11 +35,13 @@ export default {
},
},
computed: {
...mapGetters('diffs', [
...mapState(useLegacyDiffs, [
'commit',
'startVersion',
'latestVersionPath',
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapState('diffs', ['commit', 'startVersion', 'latestVersionPath']),
hasSourceVersions() {
return this.diffCompareDropdownSourceVersions.length > 0;
},
@ -92,7 +94,7 @@ export default {
},
},
methods: {
...mapActions('diffs', ['moveToNeighboringCommit']),
...mapActions(useLegacyDiffs, ['moveToNeighboringCommit']),
},
};
</script>

View File

@ -1,6 +1,6 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { mapActions } from 'pinia';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import DiffDiscussionReply from './diff_discussion_reply.vue';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
@ -42,7 +42,7 @@ export default {
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
...mapActions(useLegacyDiffs, ['showCommentForm']),
},
};
</script>

View File

@ -1,7 +1,8 @@
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import { sprintf } from '~/locale';
import { createAlert } from '~/alert';
import { mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils';
@ -14,6 +15,7 @@ import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_prev
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoteForm from '~/notes/components/note_form.vue';
import eventHub from '~/notes/event_hub';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { SAVING_THE_COMMENT_FAILED, SOMETHING_WENT_WRONG } from '../i18n';
import { getDiffMode } from '../store/utils';
@ -62,9 +64,13 @@ export default {
},
},
computed: {
...mapState('diffs', ['projectPath']),
...mapGetters('diffs', ['isInlineView', 'getCommentFormForDiffFile', 'diffLines']),
...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
...mapState(useLegacyDiffs, [
'projectPath',
'isInlineView',
'getCommentFormForDiffFile',
'diffLines',
]),
...mapVuexGetters(['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
},
@ -132,7 +138,7 @@ export default {
});
},
methods: {
...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
...mapActions(useLegacyDiffs, ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
handleSaveNote(note, parentElement, errorCallback) {
this.saveDiffDiscussion({
note,

View File

@ -1,9 +1,9 @@
<script>
import { GlIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { mapActions } from 'pinia';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
export default {
components: {
@ -38,7 +38,7 @@ export default {
},
},
methods: {
...mapActions('diffs', ['toggleFileDiscussion']),
...mapActions(useLegacyDiffs, ['toggleFileDiscussion']),
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expandedOnDiff : true;
},

View File

@ -1,10 +1,10 @@
<script>
import { GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
import { mapActions } from 'pinia';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
@ -76,7 +76,7 @@ export default {
},
},
methods: {
...mapActions('diffs', ['loadMoreLines']),
...mapActions(useLegacyDiffs, ['loadMoreLines']),
getPrevLineNumber(oldLineNumber, newLineNumber) {
const index = utils.getPreviousLineIndex(this.file, {
oldLineNumber,

View File

@ -2,7 +2,8 @@
import { GlButton, GlLoadingIcon, GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
@ -19,8 +20,8 @@ import notesEventHub from '~/notes/event_hub';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import NoteForm from '~/notes/components/note_form.vue';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { fileContentsId } from '~/diffs/components/diff_row_utils';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@ -125,9 +126,15 @@ export default {
genericError: SOMETHING_WENT_WRONG,
},
computed: {
...mapState('diffs', ['currentDiffFileId', 'conflictResolutionPath', 'canMerge']),
...mapGetters(['isLoggedIn', 'isNotesFetched', 'getNoteableData', 'noteableType']),
...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled', 'linkedFile']),
...mapState(useLegacyDiffs, [
'currentDiffFileId',
'conflictResolutionPath',
'canMerge',
'getDiffFileDiscussions',
'isVirtualScrollingEnabled',
'linkedFile',
]),
...mapVuexGetters(['isLoggedIn', 'isNotesFetched', 'getNoteableData', 'noteableType']),
autosaveKey() {
if (!this.isLoggedIn) return '';
@ -302,7 +309,7 @@ export default {
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
},
methods: {
...mapActions('diffs', [
...mapActions(useLegacyDiffs, [
'loadCollapsedDiff',
'assignDiscussionsToDiff',
'prefetchFileNeighbors',

View File

@ -13,20 +13,20 @@ import {
} from '@gitlab/ui';
import { escape } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createFileUrl, fileContentsId } from '~/diffs/components/diff_row_utils';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
import { collapsedType, isCollapsed } from '../utils/diff_file';
import { reviewable } from '../utils/file_reviews';
import DiffStats from './diff_stats.vue';
export default {
@ -99,9 +99,8 @@ export default {
},
},
computed: {
...mapState('diffs', ['latestDiff']),
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
...mapGetters(['getNoteableData']),
...mapState(useLegacyDiffs, ['latestDiff', 'diffHasExpandedDiscussions', 'diffHasDiscussions']),
...mapVuexGetters(['getNoteableData']),
diffContentIDSelector() {
return fileContentsId(this.diffFile);
},
@ -204,7 +203,7 @@ export default {
},
},
methods: {
...mapActions('diffs', [
...mapActions(useLegacyDiffs, [
'toggleFileDiscussionWrappers',
'toggleFullDiff',
'setCurrentFileHash',

View File

@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState, mapActions } from 'vuex';
import { mapState as mapVuexState, mapActions as mapVuexActions } from 'vuex';
import { mapState, mapActions } from 'pinia';
import { throttle } from 'lodash';
import { IdState } from 'vendor/vue-virtual-scroller';
import DraftNote from '~/batch_comments/components/draft_note.vue';
@ -9,6 +10,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import { hide } from '~/tooltips';
import { countLinesInBetween } from '~/diffs/utils/diff_file';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { pickDirection } from '../utils/diff_line';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
@ -69,9 +71,14 @@ export default {
};
},
computed: {
...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
...mapState('diffs', ['highlightedRow', 'coverageLoaded', 'selectedCommentPosition']),
...mapState({
...mapState(useLegacyDiffs, [
'commitId',
'fileLineCoverage',
'highlightedRow',
'coverageLoaded',
'selectedCommentPosition',
]),
...mapVuexState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
@ -95,8 +102,12 @@ export default {
this.onDragOverThrottled = throttle((line) => this.onDragOver(line), 100, { leading: true });
},
methods: {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']),
...mapVuexActions(['setSelectedCommentPosition']),
...mapActions(useLegacyDiffs, [
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
]),
showCommentLeft(line) {
return line.left && !line.right;
},

View File

@ -1,9 +1,9 @@
<script>
import { isArray } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapState } from 'pinia';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
function calcPercent(pos, renderedSize) {
return (100 * pos) / renderedSize;
@ -54,16 +54,22 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
...mapState(useLegacyDiffs, ['getDiffFileByHash', 'getCommentFormForDiffFile']),
currentCommentForm() {
return this.getCommentFormForDiffFile(this.fileHash);
},
allDiscussions() {
return isArray(this.discussions) ? this.discussions : [this.discussions];
},
formPosition() {
return {
left: `${this.currentCommentForm.xPercent}%`,
top: `${this.currentCommentForm.yPercent}%`,
};
},
},
methods: {
...mapActions('diffs', ['openDiffFileCommentForm']),
...mapActions(useLegacyDiffs, ['openDiffFileCommentForm']),
getImageDimensions() {
return {
width: Math.round(this.$parent.width),
@ -131,12 +137,6 @@ export default {
@click="clickedToggle(discussion)"
/>
<design-note-pin
v-if="canComment && currentCommentForm"
:position="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
left: `${currentCommentForm.xPercent}%`,
top: `${currentCommentForm.yPercent}%`,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
/>
<design-note-pin v-if="canComment && currentCommentForm" :position="formPosition" />
</div>
</template>

View File

@ -1,8 +1,10 @@
<script>
import { GlSprintf, GlEmptyState } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import { mapGetters as mapVuexGetters } from 'vuex';
import { mapState } from 'pinia';
import { s__, __ } from '~/locale';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
export default {
i18n: {
@ -21,11 +23,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', [
...mapState(useLegacyDiffs, [
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapGetters(['getNoteableData']),
...mapVuexGetters(['getNoteableData']),
selectedSourceVersion() {
return this.diffCompareDropdownSourceVersions.find((x) => x.selected);
},

View File

@ -26,10 +26,10 @@ export default {
v-for="(entry, index) in entries"
:key="`stacktrace-entry-${index}`"
:lines="entry.context"
:file-path="entry.filename || entry.abs_path"
:error-line="entry.lineNo"
:file-path="entry.filename || entry.abs_path || entry.absolutePath"
:error-line="entry.lineNo || entry.lineNumber"
:error-fn="entry.function"
:error-column="entry.colNo"
:error-column="entry.colNo || entry.columnNumber"
:expanded="isFirstEntry(index)"
/>
</div>

View File

@ -68,10 +68,10 @@ export default {
this.isExpanded = !this.isExpanded;
},
lineNum(line) {
return line[0];
return line[0] ?? line.lineNumber;
},
lineCode(line) {
return line[1];
return line[1] ?? line.line;
},
},
userColorScheme: window.gon.user_color_scheme,
@ -124,18 +124,16 @@ export default {
<table v-if="isExpanded" :class="$options.userColorScheme" class="code js-syntax-highlight">
<tbody>
<template v-for="(line, index) in lines">
<tr :key="`stacktrace-line-${index}`" class="line_holder">
<td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
{{ lineNum(line) }}
</td>
<td
v-safe-html="lineCode(line)"
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
></td>
</tr>
</template>
<tr v-for="(line, index) in lines" :key="`stacktrace-line-${index}`" class="line_holder">
<td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
{{ lineNum(line) }}
</td>
<td
v-safe-html="lineCode(line)"
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
></td>
</tr>
</tbody>
</table>
</div>

View File

@ -24,9 +24,6 @@ export default {
userId: this.currentUserId,
};
},
skip() {
return !this.realtimeEnabled;
},
result({ data: { userMergeRequestUpdated: mergeRequest } }) {
if (!mergeRequest) return;
@ -52,7 +49,7 @@ export default {
CollapsibleSection,
MergeRequest,
},
inject: ['mergeRequestsSearchDashboardPath', 'realtimeEnabled'],
inject: ['mergeRequestsSearchDashboardPath'],
props: {
tabs: {
type: Array,

View File

@ -148,7 +148,8 @@ export default {
});
},
updateCurrentMergeRequestIds() {
this.currentMergeRequestIds = this.mergeRequests.nodes.map((mergeRequest) => mergeRequest.id);
this.currentMergeRequestIds =
this.mergeRequests?.nodes?.map((mergeRequest) => mergeRequest.id) ?? [];
},
resetNewMergeRequestIds() {
this.updateCurrentMergeRequestIds();

View File

@ -4,6 +4,8 @@ import assigneeQuery from './queries/assignee.query.graphql';
import assigneeCountQuery from './queries/assignee_count.query.graphql';
import assigneeOrReviewerQuery from './queries/assignee_or_reviewer.query.graphql';
import assigneeOrReviewerCountQuery from './queries/assignee_or_reviewer_count.query.graphql';
import authorOrAssigneeQuery from './queries/author_or_assignee.query.graphql';
import authorOrAssigneeCountQuery from './queries/author_or_assignee_count.query.graphql';
export const QUERIES = {
reviewRequestedMergeRequests: { dataQuery: reviewerQuery, countQuery: reviewerCountQuery },
@ -12,4 +14,8 @@ export const QUERIES = {
dataQuery: assigneeOrReviewerQuery,
countQuery: assigneeOrReviewerCountQuery,
},
authorOrAssigneeMergeRequests: {
dataQuery: authorOrAssigneeQuery,
countQuery: authorOrAssigneeCountQuery,
},
};

View File

@ -118,7 +118,6 @@ export function initMergeRequestDashboard(el) {
apolloProvider,
provide: {
mergeRequestsSearchDashboardPath: el.dataset.mergeRequestsSearchDashboardPath,
realtimeEnabled: parseBoolean(el.dataset.realtimeEnabled),
},
render(createElement) {
return createElement(App, {

View File

@ -0,0 +1,37 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "./merge_request.fragment.graphql"
query requestingReviewAuthorOrAssignee(
$state: MergeRequestState = opened
$reviewState: MergeRequestReviewState
$reviewStates: [MergeRequestReviewState!]
$reviewerWildcardId: ReviewerWildcardId
$mergedAfter: Time
$not: MergeRequestsResolverNegatedParams
$perPage: Int!
$afterCursor: String
$sort: MergeRequestSort = UPDATED_DESC
) {
currentUser {
id
mergeRequests: authoredMergeRequests(
includeAssigned: true
state: $state
reviewState: $reviewState
reviewStates: $reviewStates
reviewerWildcardId: $reviewerWildcardId
mergedAfter: $mergedAfter
not: $not
first: $perPage
after: $afterCursor
sort: $sort
) {
pageInfo {
...PageInfo
}
nodes {
...MergeRequestDashboardFragment
}
}
}
}

View File

@ -0,0 +1,21 @@
query requestingReviewAuthorOrAssigneeCount(
$state: MergeRequestState = opened
$reviewState: MergeRequestReviewState
$reviewStates: [MergeRequestReviewState!]
$reviewerWildcardId: ReviewerWildcardId
$mergedAfter: Time
) {
currentUser {
id
mergeRequests: authoredMergeRequests(
includeAssigned: true
state: $state
reviewState: $reviewState
reviewStates: $reviewStates
reviewerWildcardId: $reviewerWildcardId
mergedAfter: $mergedAfter
) {
count
}
}
}

View File

@ -0,0 +1,6 @@
import Vue from 'vue';
export const badgeState = Vue.observable({
state: '',
updateStatus: null,
});

View File

@ -1,5 +1,4 @@
<script>
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import HiddenBadge from '~/issuable/components/hidden_badge.vue';
@ -9,11 +8,7 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/cons
import { fetchPolicies } from '~/lib/graphql';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
export const badgeState = Vue.observable({
state: '',
updateStatus: null,
});
import { badgeState } from '~/merge_requests/badge_state';
export default {
TYPE_ISSUE,

View File

@ -24,8 +24,9 @@ import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import TodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import SubmitReviewButton from '~/batch_comments/components/submit_review_button.vue';
import { badgeState } from '~/merge_requests/badge_state';
import titleSubscription from '../queries/title.subscription.graphql';
import { badgeState } from './merge_request_header.vue';
export default {
TYPE_MERGE_REQUEST,
@ -53,6 +54,7 @@ export default {
},
},
components: {
SubmitReviewButton,
GlIntersectionObserver,
GlLink,
GlSprintf,
@ -257,6 +259,7 @@ export default {
</ul>
<div class="gl-ml-auto gl-hidden gl-items-center lg:gl-flex">
<discussion-counter :blocks-merge="blocksMerge" hide-options />
<submit-review-button v-if="glFeatures.improvedReviewExperience" class="gl-mr-3" />
<div v-if="isSignedIn" :class="{ 'gl-flex gl-gap-3': isNotificationsTodosButtons }">
<todo-widget
:issuable-id="issuableId"

View File

@ -13,7 +13,6 @@ import {
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
import { InternalEvents } from '~/tracking';
import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
@ -21,6 +20,7 @@ import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch';
import { badgeState } from '~/merge_requests/badge_state';
import * as constants from '../constants';
import eventHub from '../event_hub';
import { COMMENT_FORM } from '../i18n';

View File

@ -11,7 +11,7 @@ import {
import { mapActions, mapState } from 'vuex';
import { InternalEvents } from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { sprintf, __ } from '~/locale';
import { __ } from '~/locale';
import { SORT_DIRECTION_UI } from '~/search/sort/constants';
import {
MR_FILTER_OPTIONS,
@ -62,12 +62,6 @@ export default {
if (length === MR_FILTER_OPTIONS.length) {
return __('All activity');
}
if (length > 1) {
return sprintf(__('%{strongStart}%{firstSelected}%{strongEnd} +%{length} more'), {
firstSelected: firstSelected.text,
length: length - 1,
});
}
return firstSelected.text;
},
@ -77,6 +71,13 @@ export default {
sortDirectionData() {
return this.isSortAsc ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
},
firstSelected() {
return MR_FILTER_OPTIONS.find(({ value }) => this.mergeRequestFilters[0] === value);
},
multipleSelected() {
const { length } = this.mergeRequestFilters;
return length > 1 && length !== MR_FILTER_OPTIONS.length;
},
},
methods: {
...mapActions(['updateMergeRequestFilters', 'setDiscussionSortDirection']),
@ -128,6 +129,7 @@ export default {
},
},
MR_FILTER_OPTIONS,
multipleSelectedPhrase: __('%{strongStart}%{firstSelected}%{strongEnd} +%{length} more'),
};
</script>
@ -161,7 +163,13 @@ export default {
>
<template #toggle>
<gl-button class="!gl-rounded-br-none !gl-rounded-tr-none">
<gl-sprintf :message="selectedFilterText">
<gl-sprintf v-if="multipleSelected" :message="$options.multipleSelectedPhrase">
<template #length>{{ mergeRequestFilters.length - 1 }}</template>
<template #strong>
<strong>{{ firstSelected.text }}</strong>
</template>
</gl-sprintf>
<gl-sprintf v-else :message="selectedFilterText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>

View File

@ -2,8 +2,8 @@ import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { isInMRPage } from '~/lib/utils/common_utils';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import { useBatchComments } from '~/batch_comments/store';
import { badgeState } from '~/merge_requests/badge_state';
import * as constants from '../../constants';
import { collapseSystemNotes } from '../../stores/collapse_utils';

View File

@ -2,7 +2,7 @@ import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { isInMRPage } from '~/lib/utils/common_utils';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import { badgeState } from '~/merge_requests/badge_state';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';

View File

@ -0,0 +1,96 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __, n__, s__ } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
export default {
name: 'ApproveUsersModal',
components: {
GlModal,
},
expose: ['show', 'hide'],
inject: ['beforeSubmitHook', 'beforeSubmitHookContexts', 'pendingUserCount'],
props: {
id: {
type: String,
required: true,
},
},
computed: {
actionPrimary() {
return {
text: n__(
'ApplicationSettings|Proceed and approve %d user',
'ApplicationSettings|Proceed and approve %d users',
this.pendingUserCount,
),
attributes: {
variant: 'confirm',
},
};
},
modal() {
return this.$refs[this.id];
},
text() {
return n__(
'ApplicationSettings|By changing this setting, you can also automatically approve %d user who is pending approval.',
'ApplicationSettings|By changing this setting, you can also automatically approve %d users who are pending approval.',
this.pendingUserCount,
);
},
},
mounted() {
this.beforeSubmitHook(this.verifyApproveUsers);
},
methods: {
show() {
this.modal.show();
},
hide() {
this.modal.hide();
},
verifyApproveUsers() {
const context = this.beforeSubmitHookContexts[this.id];
if (!context?.shouldPreventSubmit) return false;
try {
const shouldPrevent = context.shouldPreventSubmit();
if (shouldPrevent) this.show();
return shouldPrevent;
} catch (error) {
Sentry.captureException(error, {
tags: { vue_component: 'before_submit_approve_users_modal' },
});
return false;
}
},
},
modal: {
actionCancel: {
text: __('Cancel'),
},
actionSecondary: {
text: s__('ApplicationSettings|Proceed without auto-approval'),
attributes: {
category: 'secondary',
variant: 'confirm',
},
},
},
};
</script>
<template>
<gl-modal
:ref="id"
:modal-id="id"
:action-cancel="$options.modal.actionCancel"
:action-primary="actionPrimary"
:action-secondary="$options.modal.actionSecondary"
:title="s__('ApplicationSettings|Change setting and approve pending users?')"
@hide="$emit('hide')"
@primary="$emit('primary')"
@secondary="$emit('secondary')"
>{{ text }}</gl-modal
>
</template>

View File

@ -7,11 +7,11 @@ import {
GlFormRadioGroup,
GlSprintf,
GlLink,
GlModal,
} from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, n__, s__, sprintf } from '~/locale';
import { s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BeforeSubmitApproveUsersModal from './before_submit_approve_users_modal.vue';
import SignupCheckbox from './signup_checkbox.vue';
const DENYLIST_TYPE_RAW = 'raw';
@ -23,6 +23,7 @@ export default {
DENYLIST_TYPE_RAW,
DENYLIST_TYPE_FILE,
components: {
BeforeSubmitApproveUsersModal,
GlButton,
GlFormGroup,
GlFormInput,
@ -31,7 +32,6 @@ export default {
GlSprintf,
GlLink,
SignupCheckbox,
GlModal,
SeatControlSection: () =>
import(
'ee_component/pages/admin/application_settings/general/components/seat_control_section.vue'
@ -42,6 +42,14 @@ export default {
),
},
mixins: [glFeatureFlagMixin()],
provide() {
return {
beforeSubmitHook: this.addBeforeSubmitHook,
beforeSubmitHookContexts: {
[this.approveUsersModalId]: { shouldPreventSubmit: () => this.shouldShowUserApprovalModal },
},
};
},
inject: [
'host',
'settingsPath',
@ -64,7 +72,7 @@ export default {
],
data() {
return {
showModal: false,
beforeSubmitHooks: [],
canUsersBeAccidentallyApproved: false,
form: {
signupEnabled: this.signupEnabled,
@ -89,10 +97,12 @@ export default {
};
},
computed: {
approveUsersModalId() {
return 'before-submit-approve-users-modal';
},
shouldShowUserApprovalModal() {
if (!this.hasPendingUsers) return false;
if (this.hasSignupApprovalBeenToggledOff) return true;
return this.canUsersBeAccidentallyApproved;
},
hasPendingUsers() {
@ -119,56 +129,14 @@ export default {
},
);
},
approveUsersModal() {
const { pendingUserCount } = this;
return {
id: 'signup-settings-modal',
text: n__(
'ApplicationSettings|By changing this setting, you can also automatically approve %d user who is pending approval.',
'ApplicationSettings|By changing this setting, you can also automatically approve %d users who are pending approval.',
pendingUserCount,
),
actionPrimary: {
text: n__(
'ApplicationSettings|Proceed and approve %d user',
'ApplicationSettings|Proceed and approve %d users',
pendingUserCount,
),
attributes: {
variant: 'confirm',
},
},
actionSecondary: {
text: s__('ApplicationSettings|Proceed without auto-approval'),
attributes: {
category: 'secondary',
variant: 'confirm',
},
},
actionCancel: {
text: __('Cancel'),
},
};
},
},
watch: {
showModal(value) {
if (value === true) {
this.$refs[this.approveUsersModal.id].show();
} else {
this.$refs[this.approveUsersModal.id].hide();
}
},
},
methods: {
submitButtonHandler() {
if (this.shouldShowUserApprovalModal) {
this.showModal = true;
return;
}
addBeforeSubmitHook(hook) {
this.beforeSubmitHooks.push(hook);
},
handleFormSubmit() {
const shouldPreventSubmit = this.beforeSubmitHooks.some((hook) => hook());
if (shouldPreventSubmit) return;
this.submitForm();
},
setFormValue({ name, value }) {
@ -193,9 +161,6 @@ export default {
this.form.shouldProceedWithAutoApproval = false;
this.submitForm();
},
modalHideHandler() {
this.showModal = false;
},
},
i18n: {
buttonText: s__('ApplicationSettings|Save changes'),
@ -286,7 +251,6 @@ export default {
>
<gl-form-radio value="off">
{{ $options.i18n.emailConfirmationSettingsOffLabel }}
<template #help> {{ $options.i18n.emailConfirmationSettingsOffHelpText }} </template>
</gl-form-radio>
@ -298,7 +262,6 @@ export default {
<gl-form-radio value="hard">
{{ $options.i18n.emailConfirmationSettingsHardLabel }}
<template #help> {{ $options.i18n.emailConfirmationSettingsHardHelpText }} </template>
</gl-form-radio>
</gl-form-radio-group>
@ -466,23 +429,14 @@ export default {
<gl-button
data-testid="save-changes-button"
variant="confirm"
@click.prevent="submitButtonHandler"
@click.prevent="handleFormSubmit"
>{{ $options.i18n.buttonText }}</gl-button
>
{{ $options.i18n.buttonText }}
</gl-button>
<gl-modal
:ref="approveUsersModal.id"
:modal-id="approveUsersModal.id"
:action-cancel="approveUsersModal.actionCancel"
:action-primary="approveUsersModal.actionPrimary"
:action-secondary="approveUsersModal.actionSecondary"
:title="s__('ApplicationSettings|Change setting and approve pending users?')"
<before-submit-approve-users-modal
:id="approveUsersModalId"
@primary="submitFormWithAutoApproval"
@secondary="submitFormWithoutAutoApproval"
@hide="modalHideHandler"
>
{{ approveUsersModal.text }}
</gl-modal>
/>
</form>
</template>

View File

@ -11,6 +11,7 @@ import { apolloProvider } from '~/graphql_shared/issuable_client';
import { parseBoolean } from '~/lib/utils/common_utils';
import { initMrMoreDropdown } from '~/mr_more_dropdown';
import { pinia } from '~/pinia/instance';
import ReviewDrawer from '~/batch_comments/components/review_drawer.vue';
import initShow from './init_merge_request_show';
import getStateQuery from './queries/get_state.query.graphql';
@ -20,33 +21,7 @@ const tabData = Vue.observable({
tabs: [],
});
export function initMrPage() {
initMrNotes();
initShow(store, pinia);
initMrMoreDropdown();
startCodeReviewMessaging({ signalBus: diffsEventHub });
const changesCountBadge = document.querySelector('.js-changes-tab-count');
diffsEventHub.$on(EVT_MR_DIFF_GENERATED, (mergeRequestDiffGenerated) => {
const { fileCount } = mergeRequestDiffGenerated.diffStatsSummary;
if (changesCountBadge.textContent === '-') {
changesCountBadge.textContent = fileCount;
const DIFF_TAB_INDEX = 3;
const diffTab = tabData.tabs ? tabData.tabs[tabData.tabs.length - 1] : [];
const hasDiffTab = diffTab?.length >= DIFF_TAB_INDEX + 1;
if (hasDiffTab) {
diffTab[DIFF_TAB_INDEX] = fileCount;
}
}
});
}
requestIdleCallback(() => {
initSidebarBundle(store, pinia);
const initMrStickyHeader = () => {
const el = document.getElementById('js-merge-sticky-header');
if (el) {
@ -101,4 +76,52 @@ requestIdleCallback(() => {
},
});
}
});
};
const initReviewDrawer = () => {
if (!window.gon?.features?.improvedReviewExperience) return;
// Review drawer has to be located outside the MR sticky/non-sticky header
// Otherwise it will disappear when header switches between sticky/non-sticky components
const el = document.querySelector('#js-review-drawer');
// eslint-disable-next-line no-new
new Vue({
el,
pinia,
store,
render(h) {
return h(ReviewDrawer);
},
});
};
export function initMrPage() {
initMrNotes();
initShow(store, pinia);
initMrMoreDropdown();
startCodeReviewMessaging({ signalBus: diffsEventHub });
const changesCountBadge = document.querySelector('.js-changes-tab-count');
diffsEventHub.$on(EVT_MR_DIFF_GENERATED, (mergeRequestDiffGenerated) => {
const { fileCount } = mergeRequestDiffGenerated.diffStatsSummary;
if (changesCountBadge.textContent === '-') {
changesCountBadge.textContent = fileCount;
const DIFF_TAB_INDEX = 3;
const diffTab = tabData.tabs ? tabData.tabs[tabData.tabs.length - 1] : [];
const hasDiffTab = diffTab?.length >= DIFF_TAB_INDEX + 1;
if (hasDiffTab) {
diffTab[DIFF_TAB_INDEX] = fileCount;
}
}
});
requestIdleCallback(() => {
initSidebarBundle(store, pinia);
initMrStickyHeader();
initReviewDrawer();
});
}

View File

@ -11,6 +11,7 @@ import { InternalEvents } from '~/tracking';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch';
import ReviewerDrawer from '~/merge_requests/components/reviewers/reviewer_drawer.vue';
import { useBatchComments } from '~/batch_comments/store';
import eventHub from '../../event_hub';
import getMergeRequestReviewersQuery from '../../queries/get_merge_request_reviewers.query.graphql';
import mergeRequestReviewersUpdatedSubscription from '../../queries/merge_request_reviewers.subscription.graphql';
@ -130,6 +131,18 @@ export default {
canUpdate() {
return this.issuable.userPermissions?.adminMergeRequest || false;
},
isReviewer() {
const { username } = this.store?.currentUser || {};
return this.reviewers.some((reviewer) => reviewer.username === username) || false;
},
},
watch: {
isReviewer: {
handler(value) {
useBatchComments().isReviewer = value;
},
immediate: true,
},
},
created() {
this.store = new Store();

View File

@ -14,6 +14,7 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import SubmitReviewButton from '~/batch_comments/components/submit_review_button.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
import CopyEmailToClipboard from './components/copy/copy_email_to_clipboard.vue';
@ -47,6 +48,21 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
function mountSubmitReviewButton(pinia) {
if (!window.gon?.features?.improvedReviewExperience) return;
const el = document.querySelector('#js-submit-review-button');
// eslint-disable-next-line no-new
new Vue({
el,
pinia,
render(h) {
return h(SubmitReviewButton, { attrs: { class: 'gl-mr-3' } });
},
});
}
function mountSidebarTodoWidget() {
const el = document.querySelector('.js-sidebar-todo-widget-root');
@ -126,7 +142,7 @@ function mountSidebarAssigneesWidget() {
});
}
function mountSidebarReviewers(mediator) {
function mountSidebarReviewers(mediator, pinia) {
const el = document.querySelector('.js-sidebar-reviewers-root');
if (!el) {
@ -145,6 +161,7 @@ function mountSidebarReviewers(mediator) {
el,
name: 'SidebarReviewersRoot',
apolloProvider,
pinia,
provide: {
issuableIid: String(iid),
issuableId: String(id),
@ -725,7 +742,7 @@ export function mountAssigneesDropdown() {
export function mountSidebar(mediator, store, pinia) {
mountSidebarTodoWidget();
mountSidebarAssigneesWidget();
mountSidebarReviewers(mediator);
mountSidebarReviewers(mediator, pinia);
mountSidebarCrmContacts();
mountSidebarLabelsWidget();
mountSidebarMilestoneWidget();
@ -739,6 +756,7 @@ export function mountSidebar(mediator, store, pinia) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
mountSubmitReviewButton(pinia);
}
export { getSidebarOptions };

View File

@ -436,7 +436,7 @@ export default {
</div>
<div
v-if="!glFeatures.mrReportsTab && isCollapsible && !isSummaryLoading"
class="gl-border-l gl-ml-3 gl-h-6 gl-pl-3"
class="gl-border-l gl-ml-3 gl-h-6 gl-border-l-section gl-pl-3"
>
<gl-button
v-gl-tooltip

View File

@ -2,8 +2,8 @@ import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_ke
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN } from '~/issues/constants';
import { formatDate, getTimeago, newDate, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { badgeState } from '~/merge_requests/badge_state';
import {
MT_MERGE_STRATEGY,
MWCP_MERGE_STRATEGY,

View File

@ -79,7 +79,6 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemErrorTracking from './work_item_error_tracking.vue';
import WorkItemStickyHeader from './work_item_sticky_header.vue';
import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
import WorkItemTitle from './work_item_title.vue';
@ -91,6 +90,8 @@ import DesignUploadButton from './design_management/upload_button.vue';
import WorkItemDevelopment from './work_item_development/work_item_development.vue';
import WorkItemCreateBranchMergeRequestSplitButton from './work_item_development/work_item_create_branch_merge_request_split_button.vue';
const WorkItemErrorTracking = () => import('~/work_items/components/work_item_error_tracking.vue');
const defaultWorkspacePermissions = {
createDesign: false,
moveDesign: false,
@ -413,8 +414,8 @@ export default {
workItemAwardEmoji() {
return this.findWidget(WIDGET_TYPE_AWARD_EMOJI);
},
workItemErrorTrackingIdentifier() {
return this.findWidget(WIDGET_TYPE_ERROR_TRACKING)?.identifier;
workItemErrorTracking() {
return this.findWidget(WIDGET_TYPE_ERROR_TRACKING) ?? {};
},
workItemHierarchy() {
return this.findWidget(WIDGET_TYPE_HIERARCHY);
@ -1147,9 +1148,9 @@ export default {
</aside>
<work-item-error-tracking
v-if="workItemErrorTrackingIdentifier"
v-if="workItemErrorTracking.identifier"
:full-path="workItemFullPath"
:identifier="workItemErrorTrackingIdentifier"
:iid="iid"
/>
<design-widget

View File

@ -1,14 +1,18 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { GlAlert } from '@gitlab/ui';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import service from '~/error_tracking/services';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import workItemErrorTrackingQuery from '../graphql/work_item_error_tracking.query.graphql';
import { findErrorTrackingWidget } from '../utils';
const POLL_INTERVAL = 2000;
export default {
components: {
GlLoadingIcon,
CrudComponent,
GlAlert,
Stacktrace,
},
props: {
@ -16,70 +20,98 @@ export default {
type: String,
required: true,
},
identifier: {
iid: {
type: String,
required: true,
},
},
data() {
return {
errorMessage: '',
errorTracking: {},
loading: false,
stackTraceData: {},
};
},
computed: {
stackTraceEntries() {
return this.stackTraceData.stack_trace_entries?.toReversed() ?? [];
},
stackTracePath() {
return `/${this.fullPath}/-/error_tracking/${this.identifier}/stack_trace.json`;
apollo: {
errorTracking: {
query: workItemErrorTrackingQuery,
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return findErrorTrackingWidget(data.namespace.workItem) ?? {};
},
error(error) {
this.errorMessage = __('Failed to load stack trace.');
Sentry.captureException(error);
},
},
},
mounted() {
this.startPolling(this.stackTracePath);
computed: {
isLoading() {
return this.$apollo.queries.errorTracking.loading || this.loading;
},
stackTrace() {
return this.errorTracking.stackTrace?.nodes.toReversed() ?? [];
},
status() {
return this.errorTracking.status;
},
},
watch: {
// The backend fetches data in the background, so the data won't be available immediately.
// The backend returns 'RETRY' until it receives data so we need to keep polling until then.
status(status) {
switch (status) {
case 'RETRY':
this.$apollo.queries.errorTracking.startPolling(POLL_INTERVAL);
this.loading = true;
break;
case 'NOT_FOUND':
this.errorMessage = __('Sentry issue not found.');
this.stopPolling();
break;
case 'ERROR':
this.errorMessage = __('Error tracking service responded with an error.');
this.stopPolling();
break;
case 'SUCCESS':
this.stopPolling();
break;
default:
this.stopPolling();
}
},
},
beforeDestroy() {
this.stackTracePoll?.stop();
// Stop polling, for example when closing a work item drawer
this.stopPolling();
},
methods: {
startPolling(endpoint) {
this.loading = true;
this.stackTracePoll = new Poll({
resource: service,
method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
return;
}
this.stackTraceData = data.error;
this.stackTracePoll.stop();
this.loading = false;
},
errorCallback: () => {
createAlert({ message: __('Failed to load stacktrace.') });
this.loading = false;
},
});
this.stackTracePoll.makeRequest();
stopPolling() {
this.$apollo.queries.errorTracking.stopPolling();
this.loading = false;
},
},
};
</script>
<template>
<div>
<div :class="{ 'gl-border-b-0': loading }" class="card card-slim gl-mb-0 gl-mt-5">
<div class="card-header gl-border-b-0">
<h2 class="card-title gl-my-2 gl-text-base">{{ __('Stack trace') }}</h2>
</div>
</div>
<div v-if="loading" class="card gl-mb-0">
<gl-loading-icon class="gl-my-3" />
</div>
<stacktrace v-else :entries="stackTraceEntries" />
</div>
<crud-component
anchor-id="stack-trace"
is-collapsible
:is-loading="isLoading"
persist-collapsed-state
:title="__('Stack trace')"
>
<template #default>
<gl-alert v-if="errorMessage" :dismissible="false" variant="danger">
{{ errorMessage }}
</gl-alert>
<stacktrace v-if="stackTrace.length" :entries="stackTrace" />
</template>
</crud-component>
</template>

View File

@ -0,0 +1,28 @@
query workItemErrorTracking($fullPath: ID!, $iid: String!) {
namespace(fullPath: $fullPath) {
id
workItem(iid: $iid) {
id
widgets {
... on WorkItemWidgetErrorTracking {
type
identifier
stackTrace {
nodes {
absolutePath
columnNumber
context {
line
lineNumber
}
filename
function
lineNumber
}
}
status
}
}
}
}
}

View File

@ -13,6 +13,7 @@ import {
WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_DESIGNS,
WIDGET_TYPE_ERROR_TRACKING,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_LABELS,
@ -53,6 +54,9 @@ export const findDescriptionWidget = (workItem) =>
export const findDesignsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
export const findErrorTrackingWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ERROR_TRACKING);
export const findHealthStatusWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS);

View File

@ -88,7 +88,7 @@ module GraphqlTriggers
end
def self.user_merge_request_updated(user, merge_request)
return unless Feature.enabled?(:merge_request_dashboard_realtime, user, type: :wip)
return unless Feature.enabled?(:merge_request_dashboard, user, type: :wip)
GitlabSchema.subscriptions.trigger(:user_merge_request_updated, { user_id: user.to_gid }, merge_request)
end

View File

@ -13,5 +13,17 @@ module Resolvers
end
end
end
# We call this resolver from `IssueType` where object is an `Issue` instance, and we also call this resolver
# from `Widgets::DevelopmentType`, in which case the object is a connection type, so
# we need to get its respective work item.
def object
case super
when ::GraphQL::Pagination::Connection
super.try(:parent)&.work_item
else
super
end
end
end
end

View File

@ -11,6 +11,9 @@ module Types
value 'STORAGE_SIZE_ASC', 'Sort by total storage size, ascending order.', value: :storage_size_asc
value 'STORAGE_SIZE_DESC', 'Sort by total storage size, descending order.', value: :storage_size_desc
value 'PATH_ASC', 'Sort by path, ascending order.', value: :path_asc
value 'PATH_DESC', 'Sort by path, descending order.', value: :path_desc
value 'REPOSITORY_SIZE_ASC', 'Sort by total repository size, ascending order.', value: :repository_size_asc
value 'REPOSITORY_SIZE_DESC', 'Sort by total repository size, descending order.', value: :repository_size_desc

View File

@ -24,7 +24,7 @@ module Types
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release.'
field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?,
description: 'Indicates the release is an historical release.'
description: 'Indicates the release is a historical release.'
field :id, ::Types::GlobalIDType[Release],
null: false,
description: 'Global ID of the release.'

View File

@ -5,6 +5,8 @@ module Types
class ClosingMergeRequestType < BaseObject
graphql_name 'WorkItemClosingMergeRequest'
connection_type_class Types::WorkItems::Widgets::Connections::ClosingMergeRequestsConnectionType
authorize :read_merge_request_closing_issue
field :from_mr_description, GraphQL::Types::Boolean,

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
module Connections
# rubocop: disable Graphql/AuthorizeTypes -- counts are looking up authorized data already
class ClosingMergeRequestsConnectionType < GraphQL::Types::Relay::BaseConnection
graphql_name 'ClosingMergeRequestsConnectionType'
description 'Connection details for closing merge requests data'
field :count,
null: true,
description: 'Number of merge requests that close the work item on merge.',
resolver: Resolvers::MergeRequestsCountResolver
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
end
end

View File

@ -475,6 +475,9 @@ module MergeRequestsHelper
end
def merge_request_dashboard_role_based_data
is_author_or_assignee = ::Feature.enabled?(:merge_request_dashboard_author_or_assignee, current_user,
type: :gitlab_com_derisk)
{
tabs: [
{
@ -507,7 +510,7 @@ module MergeRequestsHelper
id: 'assigned',
title: _('Assigned (Active)'),
helpContent: _(''),
query: 'assignedMergeRequests',
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewStates: %w[REQUESTED_CHANGES REVIEWED],
perPage: 10
@ -518,7 +521,7 @@ module MergeRequestsHelper
title: _('Assigned (Inactive)'),
hideCount: true,
helpContent: _(''),
query: 'assignedMergeRequests',
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewStates: %w[APPROVED UNAPPROVED UNREVIEWED REVIEW_STARTED],
perPage: 10
@ -547,7 +550,7 @@ module MergeRequestsHelper
id: 'merged_recently_assigned',
title: _('Assigned'),
helpContent: _(''),
query: 'assignedMergeRequests',
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
state: 'merged',
mergedAfter: 2.weeks.ago.to_time.iso8601,
@ -562,6 +565,9 @@ module MergeRequestsHelper
end
def merge_request_dashboard_data
is_author_or_assignee = ::Feature.enabled?(:merge_request_dashboard_author_or_assignee, current_user,
type: :gitlab_com_derisk)
if Feature.enabled?(:mr_dashboard_list_type_toggle, current_user, type: :beta) &&
current_user.merge_request_dashboard_list_type == 'role_based'
return merge_request_dashboard_role_based_data
@ -578,7 +584,7 @@ module MergeRequestsHelper
id: 'returned_to_you',
title: _('Returned to you'),
helpContent: _('Reviewers left feedback, or requested changes from you, on these merge requests.'),
query: 'assignedMergeRequests',
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewStates: %w[REVIEWED REQUESTED_CHANGES]
}
@ -594,9 +600,16 @@ module MergeRequestsHelper
},
{
id: 'assigned_to_you',
title: _('Assigned to you'),
helpContent: _("You're assigned to these merge requests, but they don't have reviewers yet."),
query: 'assignedMergeRequests',
title: is_author_or_assignee ? _('Your merge requests') : _('Assigned to you'),
helpContent: if is_author_or_assignee
_("Merge requests you authored or are assigned to, " \
"without reviewers.")
else
_("You're assigned to these merge requests, but they don't have reviewers yet.")
end,
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewerWildcardId: 'NONE'
}
@ -605,11 +618,11 @@ module MergeRequestsHelper
[
{
id: 'waiting_for_assignee',
title: _('Waiting for assignee'),
title: is_author_or_assignee ? _('Waiting for author or assignee') : _('Waiting for assignee'),
hideCount: true,
helpContent: _(
'Your assigned merge requests that are waiting for approvals, ' \
'and reviews you have requested changes for.'
"Your reviews you've requested changes for " \
"or commented on."
),
query: 'reviewRequestedMergeRequests',
variables: {
@ -620,11 +633,8 @@ module MergeRequestsHelper
id: 'waiting_for_approvals',
title: _('Waiting for approvals'),
hideCount: true,
helpContent: _(
'Your assigned merge requests that are waiting for approvals, ' \
'and reviews you have requested changes for.'
),
query: 'assignedMergeRequests',
helpContent: _('Your merge requests that are waiting for approvals.'),
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewStates: %w[UNREVIEWED UNAPPROVED REVIEW_STARTED],
not: {
@ -646,8 +656,8 @@ module MergeRequestsHelper
id: 'approved_by_others',
title: _('Approved by others'),
hideCount: true,
helpContent: _('Includes all merge requests you are assigned to and a reviewer has approved.'),
query: 'assignedMergeRequests',
helpContent: _('Your merge requests with approvals by all assigned reviewers.'),
query: is_author_or_assignee ? 'authorOrAssigneeMergeRequests' : 'assignedMergeRequests',
variables: {
reviewState: 'APPROVED',
not: {

View File

@ -55,6 +55,14 @@ class DeployToken < ApplicationRecord
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
def self.prefix_for_deploy_token
return DEPLOY_TOKEN_PREFIX unless Feature.enabled?(:custom_prefix_for_all_token_types, :instance)
# Manually remove gl - we'll add this from the configuration.
# Once the feature flag has been removed, we can change DEPLOY_TOKEN_PREFIX to `ft-`
::Authn::TokenField::PrefixHelper.prepend_instance_prefix(DEPLOY_TOKEN_PREFIX.delete_prefix('gl'))
end
def valid_for_dependency_proxy?
group_type? &&
active? &&
@ -143,7 +151,7 @@ class DeployToken < ApplicationRecord
end
def prefix_for_deploy_token
DEPLOY_TOKEN_PREFIX
self.class.prefix_for_deploy_token
end
private

View File

@ -6,11 +6,13 @@ module Members
validate_access!(access_requester) unless skip_authorization
access_requester.access_level = params[:access_level] if params[:access_level]
limit_to_guest_if_billable_promotion_restricted(access_requester)
access_requester.accept_request(current_user)
after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
access_requester
success({ member: access_requester })
end
private
@ -30,6 +32,10 @@ module Members
end
end
def limit_to_guest_if_billable_promotion_restricted(access_requester)
# override in EE
end
def can_approve_access_requester?(access_requester)
can?(current_user, :admin_member_access_request, access_requester.source)
end

View File

@ -33,10 +33,8 @@
merge_requests_search_dashboard_path: merge_requests_search_dashboard_path(assignee_username: current_user.username),
initial_data: merge_request_dashboard_data.to_json,
list_type: current_user.merge_request_dashboard_list_type,
realtime_enabled: Feature.enabled?(:merge_request_dashboard_realtime, current_user, type: :wip).to_s,
list_type_toggle_enabled: Feature.enabled?(:mr_dashboard_list_type_toggle, current_user, type: :beta).to_s
} }
= gl_loading_icon(size: 'lg')
- if !merge_request_dashboard_enabled?(current_user) || current_page?(merge_requests_search_dashboard_path)
- if merge_request_dashboard_enabled?(current_user)

View File

@ -23,6 +23,7 @@
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } }
= render "projects/merge_requests/mr_title"
#js-review-drawer
#js-merge-sticky-header{ data: { data: sticky_header_data(@project, @merge_request).to_json } }
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
@ -50,6 +51,7 @@
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { class: 'js-changes-tab-count', data: { gid: @merge_request.to_gid.to_s } }
.gl-flex.gl-flex-wrap.gl-items-center.justify-content-lg-end
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
#js-submit-review-button
- if !!@issuable_sidebar.dig(:current_user, :id)
.gl-flex.gl-gap-3
.js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }

View File

@ -1,9 +1,9 @@
---
name: merge_request_dashboard_realtime
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/512629
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179560
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516000
milestone: '17.9'
name: merge_request_dashboard_author_or_assignee
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520163
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185925
rollout_issue_url:
milestone: '17.11'
group: group::code review
type: wip
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class MigrateGlobalSearchSettingsInApplicationSettingsV2 < Gitlab::Database::Migration[2.2]
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '17.11'
class ApplicationSetting < MigrationRecord
self.table_name = 'application_settings'
end
def up
ApplicationSetting.reset_column_information
application_setting = ApplicationSetting.last
return unless application_setting
# rubocop:disable Gitlab/FeatureFlagWithoutActor -- Does not execute in user context
search_settings = application_setting.search
search_settings[:global_search_block_anonymous_searches_enabled] =
Feature.enabled?(:block_anonymous_global_searches)
if Gitlab.ee?
search_settings[:global_search_limited_indexing_enabled] =
Feature.enabled?(:advanced_global_search_for_limited_indexing)
end
# rubocop:enable Gitlab/FeatureFlagWithoutActor
application_setting.update_columns(search: search_settings, updated_at: Time.current)
end
def down
application_setting = ApplicationSetting.last
return unless application_setting
search_settings_hash = application_setting.search
search_settings_hash.delete('global_search_block_anonymous_searches_enabled')
search_settings_hash.delete('global_search_limited_indexing_enabled')
application_setting.update_column(:search, search_settings_hash)
end
end

View File

@ -0,0 +1 @@
86af367e4028b2497e9cf28dc7e5b93012df947a82e1fed2d94636296a680964

View File

@ -180,7 +180,7 @@ To simplify the diagram, some necessary components are omitted.
A **secondary** site needs two different PostgreSQL databases:
- A read-only database instance that streams data from the main GitLab database.
- A [read/write database instance(tracking database)](#geo-tracking-database) used internally by the **secondary** site to record what data has been replicated.
- A [read/write database instance (tracking database)](#geo-tracking-database) used internally by the **secondary** site to record what data has been replicated.
The **secondary** sites also run an additional daemon: [Geo Log Cursor](#geo-log-cursor).

View File

@ -132,7 +132,10 @@ By default, GitLab uses `gl` as the instance prefix.
{{< alert type="note" >}}
Custom token prefixes apply only to feed tokens.
Custom token prefixes apply only to the following tokens:
- [Feed tokens](../../security/tokens/_index.md#feed-token)
- [Deploy tokens](../../user/project/deploy_tokens/_index.md)
{{< /alert >}}

View File

@ -281,7 +281,7 @@ GET /projects/:id/snippets/:snippet_id/discussions
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
```json
[
@ -384,7 +384,7 @@ Parameters:
| --------------- | -------------- | -------- | ----------- |
| `discussion_id` | integer | yes | The ID of a discussion item. |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
```shell
curl --request POST \
@ -407,7 +407,7 @@ Parameters:
| --------------- | -------------- | -------- | ----------- |
| `body` | string | yes | The content of a discussion. |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
@ -432,7 +432,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a thread. |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `note_id` | integer | yes | The ID of a thread note. |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z`. Requires administrator or project/group owner rights. |
```shell
@ -457,7 +457,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a thread. |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `note_id` | integer | yes | The ID of a thread note. |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
```shell
curl --request PUT \
@ -480,7 +480,7 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion. |
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
| `note_id` | integer | yes | The ID of a discussion note. |
| `snippet_id` | integer | yes | The ID of an snippet. |
| `snippet_id` | integer | yes | The ID of a snippet. |
```shell
curl --request DELETE \
@ -976,7 +976,7 @@ curl --request POST \
changes in the file changed the line number. For the discussion about a fix, see
[issue 32516](https://gitlab.com/gitlab-org/gitlab/-/issues/325161).
- If you specify incorrect `base`, `head`, `start`, or `SHA` parameters, you might run
into the bug described in [issue #296829)](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
into the bug described in [issue #296829](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
To create a new thread:

View File

@ -962,6 +962,6 @@ Example response:
{{< alert type="note" >}}
The `health_status` parameter can only be in an "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
The `health_status` parameter can only be in a "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
{{< /alert >}}

View File

@ -977,6 +977,6 @@ Example response:
{{< alert type="note" >}}
The `health_status` parameter can only be in an "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
The `health_status` parameter can only be in a "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
{{< /alert >}}

View File

@ -19736,6 +19736,7 @@ The connection type for [`WorkItemClosingMergeRequest`](#workitemclosingmergereq
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemclosingmergerequestconnectioncount"></a>`count` | [`Int`](#int) | Number of merge requests that close the work item on merge. |
| <a id="workitemclosingmergerequestconnectionedges"></a>`edges` | [`[WorkItemClosingMergeRequestEdge]`](#workitemclosingmergerequestedge) | A list of edges. |
| <a id="workitemclosingmergerequestconnectionnodes"></a>`nodes` | [`[WorkItemClosingMergeRequest]`](#workitemclosingmergerequest) | A list of nodes. |
| <a id="workitemclosingmergerequestconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
@ -27203,7 +27204,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="groupapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.autocompleteUsers`
@ -28124,7 +28125,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="grouppipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="grouppipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="grouppipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.projectComplianceRequirementsStatus`
@ -28321,7 +28322,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="groupscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
| <a id="groupscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="groupscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.scanResultPolicies`
@ -28343,7 +28344,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupscanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="groupscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.securityPolicyProjectSuggestions`
@ -28552,7 +28553,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="groupvulnerabilitymanagementpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="groupvulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="groupvulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Group.vulnerabilitySeveritiesCount`
@ -32528,7 +32529,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespaceapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespaceapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="namespaceapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.complianceFrameworks`
@ -32628,7 +32629,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespacepipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacepipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="namespacepipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.projects`
@ -32697,7 +32698,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="namespacescanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
| <a id="namespacescanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacescanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="namespacescanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.scanResultPolicies`
@ -32719,7 +32720,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespacescanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacescanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="namespacescanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.vulnerabilityManagementPolicies`
@ -32741,7 +32742,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="namespacevulnerabilitymanagementpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="namespacevulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="namespacevulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Namespace.workItem`
@ -34602,7 +34603,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="projectapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Project.autocompleteUsers`
@ -35889,7 +35890,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectpipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectpipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="projectpipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Project.pipelineSchedules`
@ -36113,7 +36114,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="projectscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
| <a id="projectscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="projectscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Project.scanResultPolicies`
@ -36135,7 +36136,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectscanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="projectscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Project.securityExclusion`
@ -36463,7 +36464,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectvulnerabilitymanagementpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="projectvulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
| <a id="projectvulnerabilitymanagementpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
##### `Project.vulnerabilitySeveritiesCount`
@ -37270,7 +37271,7 @@ Represents a release.
| <a id="releasedescription"></a>`description` | [`String`](#string) | Description (also known as "release notes") of the release. |
| <a id="releasedescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="releaseevidences"></a>`evidences` | [`ReleaseEvidenceConnection`](#releaseevidenceconnection) | Evidence for the release. (see [Connections](#connections)) |
| <a id="releasehistoricalrelease"></a>`historicalRelease` | [`Boolean`](#boolean) | Indicates the release is an historical release. |
| <a id="releasehistoricalrelease"></a>`historicalRelease` | [`Boolean`](#boolean) | Indicates the release is a historical release. |
| <a id="releaseid"></a>`id` | [`ReleaseID!`](#releaseid) | Global ID of the release. |
| <a id="releaselinks"></a>`links` | [`ReleaseLinks`](#releaselinks) | Links of the release. |
| <a id="releasemilestones"></a>`milestones` | [`MilestoneConnection`](#milestoneconnection) | Milestones associated to the release. (see [Connections](#connections)) |
@ -43746,6 +43747,8 @@ Values for sorting projects.
| <a id="namespaceprojectsortlfs_objects_size_desc"></a>`LFS_OBJECTS_SIZE_DESC` | Sort by total LFS object size, descending order. |
| <a id="namespaceprojectsortpackages_size_asc"></a>`PACKAGES_SIZE_ASC` | Sort by total package size, ascending order. |
| <a id="namespaceprojectsortpackages_size_desc"></a>`PACKAGES_SIZE_DESC` | Sort by total package size, descending order. |
| <a id="namespaceprojectsortpath_asc"></a>`PATH_ASC` | Sort by path, ascending order. |
| <a id="namespaceprojectsortpath_desc"></a>`PATH_DESC` | Sort by path, descending order. |
| <a id="namespaceprojectsortrepository_size_asc"></a>`REPOSITORY_SIZE_ASC` | Sort by total repository size, ascending order. |
| <a id="namespaceprojectsortrepository_size_desc"></a>`REPOSITORY_SIZE_DESC` | Sort by total repository size, descending order. |
| <a id="namespaceprojectsortsimilarity"></a>`SIMILARITY` | Most similar to the search query. |

View File

@ -75,6 +75,7 @@ Example response:
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407775) in GitLab 16.1.
- Specify a service account user username or name was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144841) in GitLab 16.10.
- Specify a service account user email address was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181456) in GitLab 17.9 [with a flag](../administration/feature_flags.md) named `group_service_account_custom_email`.
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186476) in GitLab 17.11. Feature flag `group_service_account_custom_email` removed.
{{< /history >}}

View File

@ -16,7 +16,7 @@ While GitLab offers a [built-in deployment solution](_index.md), you might prefe
GitLab can receive deployment events from these external tools and allows you to track the deployments within GitLab.
For example, the following features are available by setting up tracking:
- [See when an merge request has been deployed, and to which environment](../../user/project/merge_requests/widgets.md#post-merge-pipeline-status).
- [See when a merge request has been deployed, and to which environment](../../user/project/merge_requests/widgets.md#post-merge-pipeline-status).
- [Filter merge requests by environment or deployment date](../../user/project/merge_requests/_index.md#by-environment-or-deployment-date).
- [DevOps Research and Assessment (DORA) metrics](../../user/analytics/dora_metrics.md).
- [View environments and deployments](_index.md#view-environments-and-deployments).

View File

@ -60,7 +60,7 @@ Some terms on MLflow are named differently in GitLab:
### Setting up for testing
To test the an script with MLflow with GitLab as the backend:
To test the script with MLflow with GitLab as the backend:
1. Install MLflow:

View File

@ -12,7 +12,7 @@ class that we use for all of our database migrations. This subclass is `Gitlab::
includes all the helpers that developers can use. You can see many use cases of helpers built
in-house in [Avoiding downtime in migrations](avoiding_downtime_in_migrations.md).
Sometimes, we need to add or modify existing an helper's functionality without having a reverse effect on all the
Sometimes, we need to add or modify existing a helper's functionality without having a reverse effect on all the
previous database migrations. That's why we introduced versioning to `Gitlab::Database::Migration`. Now,
each database migration can inherit the latest version of this class at the time of the writing the database migration.
After we add a new feature, those old database migrations are no longer affected. We usually

View File

@ -468,7 +468,7 @@ RSpec.describe MergeRequests::UpdateHeadPipelineWorker do
let(:event) { pipeline_created_event }
end
# This shared example ensures that an published event is ignored. This might be useful for
# This shared example ensures that a published event is ignored. This might be useful for
# conditional dispatch testing.
it_behaves_like 'ignores the published event' do
let(:event) { pipeline_created_event }

View File

@ -148,7 +148,7 @@ See the Vuex documentation for examples of [committing mutations from components
When a request is made we often want to show a loading state to the user.
Instead of creating an mutation to toggle the loading state, we should:
Instead of creating a mutation to toggle the loading state, we should:
1. A mutation with type `REQUEST_SOMETHING`, to toggle the loading state
1. A mutation with type `RECEIVE_SOMETHING_SUCCESS`, to handle the success callback

View File

@ -143,7 +143,7 @@ An example of a single event selection rule which updates a unique count metric
### Filters
Filters are used to constrain which events cause an metric to increase.
Filters are used to constrain which events cause a metric to increase.
This filter includes only `pull_package` events with `label: rubygems`:

View File

@ -93,7 +93,7 @@ We use pytest for testing Python code. To learn more about writing and running t
1. **[Python Testing with pytest (Book)](https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition/)**
This book is a comprehensive guide to testing Python code with pytest. It covers everything from the basics of writing tests to advanced topics like fixtures, plugins, and test organization.
1. **[Python Function to flowchart)](https://gitlab.com/srayner/funcgraph/)**
1. **[Python Function to flowchart](https://gitlab.com/srayner/funcgraph/)**
This project takes any Python function and automatically creates a visual flowchart showing how the code works.
---

View File

@ -196,7 +196,7 @@ separate Redis clusters helps for two reasons:
- Each has different persistence requirements.
- Load isolation.
For example, the cache instance can behave like an least-recently used
For example, the cache instance can behave like a least-recently used
(LRU) cache by setting the `maxmemory` configuration option. That option
should not be set for the queues or persistent clusters because data
would be evicted from memory at random times. This would cause jobs to

View File

@ -779,7 +779,7 @@ To create a new Git tag to rebuild the analyzer, follow these steps:
This should be done on the **18th of each month**. Though, this is a soft deadline and there is no harm in doing it within a few days after.
First, create an new issue for a release with a script from this repo: `./scripts/release_issue.rb MAJOR.MINOR`.
First, create a new issue for a release with a script from this repo: `./scripts/release_issue.rb MAJOR.MINOR`.
This issue will guide you through the whole release process. In general, you have to perform the following tasks:
- Check the list of supported technologies in GitLab documentation.

View File

@ -77,7 +77,7 @@ difficult to achieve locally. Ordering issues are easier to reproduce by repeate
any table has more than 500 columns. It could pass in the merge request, but fail later in
`master` if the order of tests changes.
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91016/diffs): A test asserts
that trying to find a record with an nonexistent ID returns an error message. The test uses an
that trying to find a record with a nonexistent ID returns an error message. The test uses an
hardcoded ID that's supposed to not exist (for example, `42`). If the test is run early in the test
suite, it might pass as not enough records were created before it, but as soon as it would run
later in the suite, there could be a record that actually has the ID `42`, hence the test would

View File

@ -114,14 +114,14 @@ participant Rails
participant Gitaly
Note left of Git on client: git clone/fetch
Git on client->>+Workhorse: GET /foo/bar.git/info/refs/?service=git-upload-pack
Git on client->>+Workhorse: GET /foo/bar.git/info/refs?service=git-upload-pack
Workhorse->>+Rails: GET Repositories::GitHttpController#info_refs
Note right of Rails: Access check/Log activity
Rails-->>Workhorse: 200 OK, Gitlab::Workhorse.git_http_ok
Workhorse->>+Gitaly: SmartHTTPService.InfoRefsUploadPack gRPC request
Gitaly -->>-Workhorse: SmartHTTPService.InfoRefsUploadPack gRPC response
Workhorse-->>-Git on client: send info-refs response
Git on client->>+Workhorse: GET /foo/bar.git/info/refs/?service=git-upload-pack
Git on client->>+Workhorse: GET /foo/bar.git/info/refs?service=git-upload-pack
Workhorse->>+Rails: GET Repositories::GitHttpController#git_receive_pack
Note right of Rails: Access check/Update statistics
Rails-->>Workhorse: 200 OK, Gitlab::Workhorse.git_http_ok
@ -141,14 +141,14 @@ participant Rails
participant Gitaly
Note left of Git on client: git push
Git on client->>+Workhorse: GET /foo/bar.git/info/refs/?service=git-receive-pack
Git on client->>+Workhorse: GET /foo/bar.git/info/refs?service=git-receive-pack
Workhorse->>+Rails: GET Repositories::GitHttpController#info_refs
Note right of Rails: Access check/Log activity
Rails-->>Workhorse: 200 OK, Gitlab::Workhorse.git_http_ok
Workhorse->>+Gitaly: SmartHTTPService.InfoRefsReceivePack gRPC request
Gitaly -->>-Workhorse: SmartHTTPService.InfoRefsReceivePack gRPC response
Workhorse-->>-Git on client: send info-refs response
Git on client->>+Workhorse: GET /foo/bar.git/info/refs/?service=git-receive-pack
Git on client->>+Workhorse: GET /foo/bar.git/info/refs?service=git-receive-pack
Workhorse->>+Rails: GET Repositories::GitHttpController#git_receive_pack
Note right of Rails: Access check/Update statistics
Rails-->>Workhorse: 200 OK, Gitlab::Workhorse.git_http_ok

View File

@ -65,7 +65,7 @@ Users with phone numbers from unsupported countries can try [credit card verific
### Partially supported countries
A user might not receive a one-time password (OTP) if their phone number is from an partially supported country. Whether a message is delivered depends on country enforcement and regulation.
A user might not receive a one-time password (OTP) if their phone number is from a partially supported country. Whether a message is delivered depends on country enforcement and regulation.
The following countries are partially supported:

View File

@ -33,7 +33,7 @@ Sign in with an existing Amazon account or create a new one.
## Create a Kubernetes cluster
To create an new cluster on Amazon EKS:
To create a new cluster on Amazon EKS:
- Follow the steps in [Create an Amazon EKS cluster](../../../user/infrastructure/clusters/connect/new_eks_cluster.md).

View File

@ -378,7 +378,7 @@ Some scanners behave differently in a `scan` action than they do in a regular CI
scan.
- Static Application Security Testing (SAST): Runs only if the repository contains
[files supported by SAST)](../sast/_index.md#supported-languages-and-frameworks).
[files supported by SAST](../sast/_index.md#supported-languages-and-frameworks).
- Secret detection:
- Only rules with the default ruleset are supported.
[Replacing](../secret_detection/pipeline/configure.md#replace-the-default-ruleset) or [extending](../secret_detection/pipeline/configure.md#extend-the-default-ruleset)

View File

@ -53,7 +53,7 @@ Different features are available in different [GitLab tiers](https://about.gitla
Pipeline secret detection is optimized to balance coverage and run time.
Only the current state of the repository and future commits are scanned for secrets.
To identify secrets already present in the repository's history, run an historic scan once
To identify secrets already present in the repository's history, run a historic scan once
after enabling pipeline secret detection. Scan results are available only after the pipeline is completed.
Exactly what is scanned for secrets depends on the type of pipeline,

View File

@ -60,7 +60,7 @@ Each dashboard provides a unique viewpoint of your security posture.
### Project Security Dashboard
The Project Security Dashboard shows the total number of vulnerabilities detected over time,
with up to 365 days of historical data for a given project. The dashboard is an historical view of open vulnerabilities in the default branch. Open vulnerabilities are those of only `Needs triage` or `Confirmed` status (`Dismissed` or `Resolved` vulnerabilities are excluded).
with up to 365 days of historical data for a given project. The dashboard is a historical view of open vulnerabilities in the default branch. Open vulnerabilities are those of only `Needs triage` or `Confirmed` status (`Dismissed` or `Resolved` vulnerabilities are excluded).
To view a project's security dashboard:

View File

@ -281,13 +281,10 @@ To link the SAML groups:
1. Select **Save**.
1. Repeat to add additional group links if required.
![SAML Group Links](img/saml_group_links_v17_8.png)
## Manage GitLab Duo seat assignment
{{< details >}}
- Tier: Premium, Ultimate
- Offering: GitLab.com
{{< /details >}}
@ -344,9 +341,7 @@ To avoid this issue, you can use the Azure AD integration, which:
- Supports only Group Links configured with group unique identifiers (like `12345678-9abc-def0-1234-56789abcde`)
when it processes Group Sync.
Alternatively, you can change the [group claims](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-group-claims#configure-the-microsoft-entra-application-registration-for-group-attributes) to use the **Groups assigned to the application** option.
![Manage Group Claims](img/Azure-manage-group-claims_v15_9.png)
Alternatively, you can change the [group claims](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-group-claims) to use the **Groups assigned to the application** option.
### Configure Azure AD
@ -426,7 +421,6 @@ To configure for GitLab Self-Managed:
{{< details >}}
- Tier: Premium, Ultimate
- Offering: GitLab Self-Managed, GitLab Dedicated
{{< /details >}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -512,7 +512,7 @@ Troubleshooting sections.
#### 422 error with non-allowed email
You might get an 422 error that states "Email is not allowed for sign-up. Please use your regular email address."
You might get a 422 error that states "Email is not allowed for sign-up. Please use your regular email address."
This message might indicate that you must add or remove a domain from your domain allowlist or denylist settings.

View File

@ -32,7 +32,7 @@ To access your user settings:
## Generate or change your Support PIN
GitLab Support may ask for an personal identification number (PIN) to validate your identity.
GitLab Support may ask for a personal identification number (PIN) to validate your identity.
The PIN expires seven days after creation.
To generate a new Support PIN:

View File

@ -65,7 +65,7 @@ To clear the cache:
## Base domain
Specifying a base domain automatically sets `KUBE_INGRESS_BASE_DOMAIN` as an deployment variable.
Specifying a base domain automatically sets `KUBE_INGRESS_BASE_DOMAIN` as a deployment variable.
If you are using [Auto DevOps](../../../topics/autodevops/_index.md), this domain is used for the different
stages. For example, Auto Review Apps and Auto Deploy.
@ -90,7 +90,7 @@ To determine the external Ingress IP address, or external Ingress hostname:
cluster. This information can then be used to set up DNS entries and forwarding
rules that allow external access to your deployed applications.
Depending an your Ingress, the external IP address can be retrieved in various ways.
Depending on your Ingress, the external IP address can be retrieved in various ways.
This list provides a generic solution, and some GitLab-specific approaches:
- In general, you can list the IP addresses of all load balancers by running:

View File

@ -246,7 +246,7 @@ open and watch updates in real time or you can return to it later.
To cancel imports that are pending or in progress, next to the imported project, select **Cancel**.
If the import has already started, the imported files are kept.
To open an repository in GitLab URL after it has been imported, select its GitLab path.
To open a repository in GitLab URL after it has been imported, select its GitLab path.
Completed imports can be re-imported by selecting **Re-import** and specifying new name. This creates a new copy of the source project.

View File

@ -20,7 +20,7 @@ that can provide useful signals.
### CPU and Memory
There are two main RPCs that handle clones/fetches. The following log entry
fields an be used to inspect how much system resources are consumed by
fields can be used to inspect how much system resources are consumed by
clones/fetches for a given repository.
The following are log entry fields in the Gitaly logs that can be filtered on:

View File

@ -1254,7 +1254,7 @@ or completely separately.
{{< /tabs >}}
1. [Deprecated in GitLab 16.0 and planned for removal in 19.0)](../../../update/deprecations.md#sidekiq-delivery-method-for-incoming_email-and-service_desk_email-is-deprecated):
1. [Deprecated in GitLab 16.0 and planned for removal in 19.0](../../../update/deprecations.md#sidekiq-delivery-method-for-incoming_email-and-service_desk_email-is-deprecated):
If you experience issues with the `webhook` setup, use `sidekiq` to deliver the email payload directly to GitLab Sidekiq using Redis.
{{< tabs >}}

View File

@ -99,12 +99,12 @@ module API
source = find_source(source_type, params[:id])
access_requester = source.requesters.find_by!(user_id: params[:user_id])
member = ::Members::ApproveAccessRequestService
result = ::Members::ApproveAccessRequestService
.new(current_user, declared_params)
.execute(access_requester)
status :created
present member, with: Entities::Member
present result[:member], with: Entities::Member
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -603,7 +603,7 @@ module API
get ':id/merge_requests/:merge_request_iid/diffs', feature_category: :code_review_workflow, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.merge_request_diff.paginated_diffs(params[:page], params[:per_page])).diffs, with: Entities::Diff, enable_unidiff: declared_params[:unidiff]
present merge_request.merge_request_diff.paginated_diffs(params[:page], params[:per_page]).diffs, with: Entities::Diff, enable_unidiff: declared_params[:unidiff]
end
desc 'Get the merge request raw diffs' do

View File

@ -4,7 +4,10 @@ module Authn
module Tokens
class DeployToken
def self.prefix?(plaintext)
plaintext.start_with?(::DeployToken::DEPLOY_TOKEN_PREFIX)
deploy_token_prefixes = [::DeployToken.prefix_for_deploy_token,
Authn::TokenField::PrefixHelper.default_instance_prefix(::DeployToken.prefix_for_deploy_token)]
plaintext.start_with?(*deploy_token_prefixes)
end
attr_reader :revocable, :source

View File

@ -69,7 +69,7 @@ module Gitlab
if ([version]"#{GLAB_REQUIRED_VERSION}" -le [version]$glabVersion) {
#{GLAB_ENV_SET_WINDOWS}
#{GLAB_LOGIN_WINDOWS}
#{glab_create_command(GLAB_CREATE_WINDOWS)}
#{glab_create_command('windows')}
}
else {
Write-Output "#{GLAB_WARNING_MESSAGE}"
@ -89,7 +89,7 @@ module Gitlab
if [ "$(printf "%s\n%s" "#{GLAB_REQUIRED_VERSION}" "$(glab --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" | sort -V | head -n1)" = "#{GLAB_REQUIRED_VERSION}" ]; then
#{GLAB_ENV_SET_UNIX}
#{GLAB_LOGIN_UNIX}
#{glab_create_command(GLAB_CREATE_UNIX)}
#{glab_create_command('unix')}
else
echo "#{GLAB_WARNING_MESSAGE}"
@ -116,9 +116,22 @@ module Gitlab
command.freeze
end
def glab_create_command(base_command)
command = base_command.dup
command.concat(" \"#{config[:tag_name]}\"")
def glab_create_command(platform) # rubocop:disable Metrics/ -- It's more readable this way
if platform == 'windows'
command = GLAB_CREATE_WINDOWS.dup
# More information: https://gitlab.com/groups/gitlab-org/-/epics/15437#note_2432564707
tag_name = config[:tag_name].presence || '$env:CI_COMMIT_TAG'
ref = config[:ref].presence || '$env:CI_COMMIT_SHA'
else
command = GLAB_CREATE_UNIX.dup
# More information: https://gitlab.com/groups/gitlab-org/-/epics/15437#note_2432564707
tag_name = config[:tag_name].presence || '$CI_COMMIT_TAG'
ref = config[:ref].presence || '$CI_COMMIT_SHA'
end
command.concat(" \"#{tag_name}\"")
command.concat(" --assets-links #{stringified_json(create_asset_links)}") if create_asset_links.present?
command.concat(" --milestone \"#{config[:milestones].join(',')}\"") if config[:milestones].present?
command.concat(" --name \"#{config[:name]}\"") if config[:name].present?
@ -128,7 +141,7 @@ module Gitlab
command.concat(" --experimental-notes-text-or-file \"#{config[:description]}\"")
end
command.concat(" --ref \"#{config[:ref]}\"") if config[:ref].present?
command.concat(" --ref \"#{ref}\"") if ref.present?
command.concat(" --tag-message \"#{config[:tag_message]}\"") if config[:tag_message].present?
command.concat(" --released-at \"#{config[:released_at]}\"") if config[:released_at].present?

View File

@ -27,7 +27,7 @@ module Gitlab
page ||= DEFAULT_PAGE
per_page ||= DEFAULT_PER_PAGE
relation.page(page).per([per_page.to_i, DEFAULT_PER_PAGE].min)
relation.page(page).per(per_page.to_i)
end
end
end

View File

@ -24103,6 +24103,9 @@ msgstr ""
msgid "Error tracking"
msgstr ""
msgid "Error tracking service responded with an error."
msgstr ""
msgid "Error updating %{issuableType}"
msgstr ""
@ -25172,6 +25175,9 @@ msgstr ""
msgid "Failed to load related branches"
msgstr ""
msgid "Failed to load stack trace."
msgstr ""
msgid "Failed to load stacktrace."
msgstr ""
@ -31399,9 +31405,6 @@ msgstr ""
msgid "Includes LFS objects. It can be overridden per group, or per project. Set to 0 for no limit."
msgstr ""
msgid "Includes all merge requests you are assigned to and a reviewer has approved."
msgstr ""
msgid "Includes an MVC structure to help you get started"
msgstr ""
@ -36856,6 +36859,9 @@ msgstr ""
msgid "Merge requests can't be merged if the status checks did not succeed or are still running."
msgstr ""
msgid "Merge requests you authored or are assigned to, without reviewers."
msgstr ""
msgid "Merge train pipelines continue without the merged changes."
msgstr ""
@ -55300,6 +55306,9 @@ msgstr ""
msgid "Sentry event"
msgstr ""
msgid "Sentry issue not found."
msgstr ""
msgid "Sep"
msgstr ""
@ -65952,6 +65961,9 @@ msgstr ""
msgid "Vulnerability|Request/Response"
msgstr ""
msgid "Vulnerability|Resolve the vulnerability in these dependencies to see additional paths. GitLab shows a maximum of 20 dependency paths per vulnerability."
msgstr ""
msgid "Vulnerability|Scanner:"
msgstr ""
@ -66078,6 +66090,9 @@ msgstr ""
msgid "Waiting for assignee"
msgstr ""
msgid "Waiting for author or assignee"
msgstr ""
msgid "Waiting for merge (open and assigned)"
msgstr ""
@ -69310,9 +69325,6 @@ msgstr ""
msgid "Your applications"
msgstr ""
msgid "Your assigned merge requests that are waiting for approvals, and reviews you have requested changes for."
msgstr ""
msgid "Your authorized applications"
msgstr ""
@ -69411,9 +69423,18 @@ msgstr ""
msgid "Your membership will expire in %{days_to_expire} days"
msgstr ""
msgid "Your merge requests"
msgstr ""
msgid "Your merge requests have a new homepage!"
msgstr ""
msgid "Your merge requests that are waiting for approvals."
msgstr ""
msgid "Your merge requests with approvals by all assigned reviewers."
msgstr ""
msgid "Your name"
msgstr ""
@ -69486,6 +69507,9 @@ msgstr ""
msgid "Your review"
msgstr ""
msgid "Your reviews you've requested changes for or commented on."
msgstr ""
msgid "Your search didn't match any commits. Try a different query."
msgstr ""

View File

@ -305,7 +305,7 @@
"swagger-cli": "^4.0.4",
"tailwindcss": "^3.4.1",
"timezone-mock": "^1.0.8",
"vite": "^6.2.4",
"vite": "^6.2.5",
"vite-plugin-ruby": "^5.1.1",
"vue-loader-vue3": "npm:vue-loader@17.4.2",
"vue-test-utils-compat": "0.0.14",

View File

@ -12,6 +12,7 @@ RSpec.describe 'Merge request > User sees merge request file tree sidebar', :js,
let(:sidebar_scroller) { sidebar.find('.vue-recycle-scroller') }
before do
stub_feature_flags(improved_review_experience: false)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
wait_for_requests

View File

@ -0,0 +1,131 @@
import { GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BeforeSubmitApproveUsersModal from '~/pages/admin/application_settings/general/components/before_submit_approve_users_modal.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { stubComponent } from 'helpers/stub_component';
jest.mock('~/sentry/sentry_browser_wrapper');
describe('BeforeSubmitApproveUsersModal', () => {
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
let beforeSubmitHook;
let beforeSubmitHookContexts = {};
const modalId = 'before-submit-modal-id';
const findModal = () => wrapper.findComponent(GlModal);
const verifyApproveUsers = () => beforeSubmitHook.mock.calls[0][0]();
const modalStub = { show: jest.fn(), hide: jest.fn() };
const GlModalStub = stubComponent(GlModal, { methods: modalStub });
const createComponent = ({ provide = {} } = {}) => {
beforeSubmitHook = jest.fn();
wrapper = shallowMountExtended(BeforeSubmitApproveUsersModal, {
propsData: { id: modalId },
provide: {
beforeSubmitHook,
beforeSubmitHookContexts,
pendingUserCount: 10,
...provide,
},
stubs: {
GlModal: GlModalStub,
},
});
};
describe('with should prevent submit', () => {
beforeEach(() => {
beforeSubmitHookContexts = { [modalId]: { shouldPreventSubmit: () => true } };
createComponent();
verifyApproveUsers();
});
it('shows the modal', () => {
expect(modalStub.show).toHaveBeenCalled();
});
it('shows a title', () => {
expect(findModal().props('title')).toBe('Change setting and approve pending users?');
});
it('shows a text', () => {
expect(wrapper.text()).toBe(
'By changing this setting, you can also automatically approve 10 users who are pending approval.',
);
});
it('shows a confirm button', () => {
expect(findModal().props('actionPrimary').text).toBe('Proceed and approve 10 users');
});
it('shows a secondary button', () => {
expect(findModal().props('actionSecondary').text).toBe('Proceed without auto-approval');
});
it('shows a cancel button', () => {
expect(findModal().props('actionCancel').text).toBe('Cancel');
});
it('registers the hook', () => {
expect(beforeSubmitHook).toHaveBeenCalledWith(expect.any(Function));
});
it.each(['hide', 'primary', 'secondary'])('emits %s event', (event) => {
findModal().vm.$emit(event);
expect(wrapper.emitted(event)).toHaveLength(1);
});
});
describe('when the before submit hook has no reference ID', () => {
it('does not show the modal', () => {
beforeSubmitHookContexts = {};
createComponent();
verifyApproveUsers();
expect(modalStub.show).not.toHaveBeenCalled();
});
});
describe('with should prevent submit to false', () => {
it('does not show the modal', () => {
beforeSubmitHookContexts = { [modalId]: { shouldPreventSubmit: () => false } };
createComponent();
verifyApproveUsers();
expect(modalStub.show).not.toHaveBeenCalled();
});
});
describe('with should prevent submit not provided', () => {
it('does not show the modal', () => {
beforeSubmitHookContexts = { [modalId]: {} };
createComponent();
verifyApproveUsers();
expect(modalStub.show).not.toHaveBeenCalled();
});
});
describe('with should prevent raising an error', () => {
it('captures the error with Sentry', () => {
const error = new Error('This is an error');
beforeSubmitHookContexts = {
[modalId]: {
shouldPreventSubmit: () => {
throw error;
},
},
};
createComponent();
verifyApproveUsers();
expect(Sentry.captureException).toHaveBeenCalledWith(error, {
tags: { vue_component: 'before_submit_approve_users_modal' },
});
});
});
});

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