diff --git a/.haml-lint.yml b/.haml-lint.yml index faa858687b7..fbd85412637 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -30,7 +30,9 @@ linters: max_consecutive: 2 DocumentationLinks: - enabled: true + # Will be enabled again after offenses are resolved. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/419673 + enabled: false include: - 'app/views/**/*.haml' - 'ee/app/views/**/*.haml' diff --git a/.rubocop_todo/gitlab/feature_flag_without_actor.yml b/.rubocop_todo/gitlab/feature_flag_without_actor.yml index b0d5aa61bb8..1d9feedd44d 100644 --- a/.rubocop_todo/gitlab/feature_flag_without_actor.yml +++ b/.rubocop_todo/gitlab/feature_flag_without_actor.yml @@ -205,7 +205,7 @@ Gitlab/FeatureFlagWithoutActor: - 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb' - 'lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch.rb' - 'lib/gitlab/database/reindexing.rb' - - 'lib/gitlab/experiment/rollout/feature.rb' + - 'lib/gitlab/experiment_feature_rollout.rb' - 'lib/gitlab/git/diff.rb' - 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/user.rb' diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 9f0c960d769..3153bf08f25 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -1088,7 +1088,7 @@ Gitlab/NamespacedClass: - 'lib/gitlab/environment_logger.rb' - 'lib/gitlab/exceptions_app.rb' - 'lib/gitlab/exclusive_lease.rb' - - 'lib/gitlab/experiment/rollout/feature.rb' + - 'lib/gitlab/experiment_feature_rollout.rb' - 'lib/gitlab/fake_application_settings.rb' - 'lib/gitlab/favicon.rb' - 'lib/gitlab/feature_categories.rb' diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue index 1c7b7f09f34..c3c2184b904 100644 --- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue @@ -55,7 +55,7 @@ export default { return getIdFromGraphQLId(this.author.id); }, showEditButton() { - return this.note.userPermissions.resolveNote; + return false; }, editedAtClasses() { return this.showReplyButton ? 'gl-text-secondary gl-pl-3' : 'gl-text-secondary gl-pl-8'; diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql index 84b57b4ed79..d75c3f1bc51 100644 --- a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql @@ -1,7 +1,6 @@ #import "~/graphql_shared/fragments/author.fragment.graphql" -#import "./abuse_report_note_permissions.fragment.graphql" -fragment AbuseReportNote on Note { +fragment AbuseReportNote on AbuseReportNote { id body bodyHtml @@ -16,9 +15,6 @@ fragment AbuseReportNote on Note { ...Author webPath } - userPermissions { - ...AbuseReportNotePermissions - } discussion { id notes { diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql deleted file mode 100644 index 31ca24e675f..00000000000 --- a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql +++ /dev/null @@ -1,3 +0,0 @@ -fragment AbuseReportNotePermissions on NotePermissions { - resolveNote -} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql index 53ac9468e08..bc97e38ca63 100644 --- a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql @@ -1,16 +1,9 @@ -#import "./abuse_report_note.fragment.graphql" - mutation createAbuseReportNote($input: CreateNoteInput!) { createNote(input: $input) { note { id discussion { id - notes { - nodes { - ...AbuseReportNote - } - } } } errors diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql index e11165074c9..b0a0a48025e 100644 --- a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql @@ -1,10 +1,5 @@ -#import "./abuse_report_note.fragment.graphql" - mutation updateAbuseReportNote($input: UpdateNoteInput!) { updateNote(input: $input) { - note { - ...AbuseReportNote - } errors } } diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue index ad1493679d1..b8333c55712 100644 --- a/app/assets/javascripts/ci/common/private/job_action_component.vue +++ b/app/assets/javascripts/ci/common/private/job_action_component.vue @@ -98,6 +98,7 @@ export default { .post(`${this.link}.json`) .then(() => { this.isLoading = false; + this.isDisabled = false; this.$emit('pipelineActionRequestComplete'); }) diff --git a/app/assets/javascripts/diffs/stores/legacy_diffs/actions.js b/app/assets/javascripts/diffs/stores/legacy_diffs/actions.js index d5fd55f4214..a134a7273b2 100644 --- a/app/assets/javascripts/diffs/stores/legacy_diffs/actions.js +++ b/app/assets/javascripts/diffs/stores/legacy_diffs/actions.js @@ -21,6 +21,7 @@ import { generateTreeList } from '~/diffs/utils/tree_worker_utils'; import { sortTree } from '~/ide/stores/utils'; import { detectAndConfirmSensitiveTokens } from '~/lib/utils/secret_detection'; import { isCollapsed } from '~/diffs/utils/diff_file'; +import { useNotes } from '~/notes/store/legacy_notes'; import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, @@ -80,7 +81,7 @@ import { findDiffFile, } from '../../store/utils'; -export const setBaseConfig = ({ commit }, options) => { +export function setBaseConfig(options) { const { endpoint, endpointMetadata, @@ -97,7 +98,7 @@ export const setBaseConfig = ({ commit }, options) => { diffViewType, perPage, } = options; - commit(types.SET_BASE_CONFIG, { + this[types.SET_BASE_CONFIG]({ endpoint, endpointMetadata, endpointBatch, @@ -117,12 +118,12 @@ export const setBaseConfig = ({ commit }, options) => { Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => { const viewedId = id.replace(/^hash:/, ''); - commit(types.SET_DIFF_FILE_VIEWED, { id: viewedId, seen: true }); + this[types.SET_DIFF_FILE_VIEWED]({ id: viewedId, seen: true }); }); -}; +} -export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry) => { - const url = new URL(state.endpointBatch, 'https://gitlab.com'); +export async function prefetchSingleFile(treeEntry) { + const url = new URL(this.endpointBatch, 'https://gitlab.com'); const diffId = getParameterValues('diff_id', url)[0]; const startSha = getParameterValues('start_sha', url)[0]; @@ -130,14 +131,14 @@ export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry) treeEntry && !treeEntry.diffLoaded && !treeEntry.diffLoading && - !getters.getDiffFileByHash(treeEntry.fileHash) + !this.getDiffFileByHash(treeEntry.fileHash) ) { const urlParams = { old_path: treeEntry.filePaths.old, new_path: treeEntry.filePaths.new, - w: state.showWhitespace ? '0' : '1', + w: this.showWhitespace ? '0' : '1', view: 'inline', - commit_id: getters.commitId, + commit_id: this.commitId, diff_head: true, }; @@ -149,45 +150,45 @@ export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry) urlParams.start_sha = startSha; } - commit(types.TREE_ENTRY_DIFF_LOADING, { path: treeEntry.filePaths.new }); + this[types.TREE_ENTRY_DIFF_LOADING]({ path: treeEntry.filePaths.new }); try { const { data: diffData } = await axios.get( - mergeUrlParams({ ...urlParams }, state.endpointDiffForPath), + mergeUrlParams({ ...urlParams }, this.endpointDiffForPath), ); - commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files }); + this[types.SET_DIFF_DATA_BATCH]({ diff_files: diffData.diff_files }); eventHub.$emit('diffFilesModified'); } catch (e) { - commit(types.TREE_ENTRY_DIFF_LOADING, { path: treeEntry.filePaths.new, loading: false }); + this[types.TREE_ENTRY_DIFF_LOADING]({ path: treeEntry.filePaths.new, loading: false }); } } -}; +} -export const fetchFileByFile = async ({ state, getters, commit }) => { +export async function fetchFileByFile() { const isNoteLink = isUrlHashNoteLink(window?.location?.hash); - const id = parseUrlHashAsFileHash(window?.location?.hash, state.currentDiffFileId); - const url = new URL(state.endpointBatch, 'https://gitlab.com'); + const id = parseUrlHashAsFileHash(window?.location?.hash, this.currentDiffFileId); + const url = new URL(this.endpointBatch, 'https://gitlab.com'); const diffId = getParameterValues('diff_id', url)[0]; const startSha = getParameterValues('start_sha', url)[0]; const treeEntry = id - ? getters.flatBlobsList.find(({ fileHash }) => fileHash === id) - : getters.flatBlobsList[0]; + ? this.flatBlobsList.find(({ fileHash }) => fileHash === id) + : this.flatBlobsList[0]; eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START); - if (treeEntry && !treeEntry.diffLoaded && !getters.getDiffFileByHash(id)) { + if (treeEntry && !treeEntry.diffLoaded && !this.getDiffFileByHash(id)) { // Overloading "batch" loading indicators so the UI stays mostly the same - commit(types.SET_BATCH_LOADING_STATE, 'loading'); - commit(types.SET_RETRIEVING_BATCHES, true); + this[types.SET_BATCH_LOADING_STATE]('loading'); + this[types.SET_RETRIEVING_BATCHES](true); const urlParams = { old_path: treeEntry.filePaths.old, new_path: treeEntry.filePaths.new, - w: state.showWhitespace ? '0' : '1', + w: this.showWhitespace ? '0' : '1', view: 'inline', - commit_id: getters.commitId, + commit_id: this.commitId, diff_head: true, }; @@ -200,35 +201,35 @@ export const fetchFileByFile = async ({ state, getters, commit }) => { } axios - .get(mergeUrlParams({ ...urlParams }, state.endpointDiffForPath)) + .get(mergeUrlParams({ ...urlParams }, this.endpointDiffForPath)) .then(({ data: diffData }) => { - commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files }); + this[types.SET_DIFF_DATA_BATCH]({ diff_files: diffData.diff_files }); - if (!isNoteLink && !state.currentDiffFileId) { - commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0]?.file_hash || ''); + if (!isNoteLink && !this.currentDiffFileId) { + this[types.SET_CURRENT_DIFF_FILE](this.diffFiles[0]?.file_hash || ''); } - commit(types.SET_BATCH_LOADING_STATE, 'loaded'); + this[types.SET_BATCH_LOADING_STATE]('loaded'); eventHub.$emit('diffFilesModified'); }) .catch(() => { - commit(types.SET_BATCH_LOADING_STATE, 'error'); + this[types.SET_BATCH_LOADING_STATE]('error'); }) .finally(() => { - commit(types.SET_RETRIEVING_BATCHES, false); + this[types.SET_RETRIEVING_BATCHES](false); }); } -}; +} -export const fetchDiffFilesBatch = ({ commit, state, dispatch }, linkedFileLoading = false) => { - let perPage = state.viewDiffsFileByFile ? 1 : state.perPage; +export function fetchDiffFilesBatch(linkedFileLoading = false) { + let perPage = this.viewDiffsFileByFile ? 1 : this.perPage; let increaseAmount = 1.4; const startPage = 0; const id = window?.location?.hash; const isNoteLink = id.indexOf('#note') === 0; const urlParams = { - w: state.showWhitespace ? '0' : '1', + w: this.showWhitespace ? '0' : '1', view: 'inline', }; const hash = window.location.hash.replace('#', '').split('diff-content-').pop(); @@ -236,22 +237,22 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }, linkedFileLoadi let scrolledVirtualScroller = hash === ''; if (!linkedFileLoading) { - commit(types.SET_BATCH_LOADING_STATE, 'loading'); - commit(types.SET_RETRIEVING_BATCHES, true); + this[types.SET_BATCH_LOADING_STATE]('loading'); + this[types.SET_RETRIEVING_BATCHES](true); } eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START); const getBatch = (page = startPage) => axios - .get(mergeUrlParams({ ...urlParams, page, per_page: perPage }, state.endpointBatch)) + .get(mergeUrlParams({ ...urlParams, page, per_page: perPage }, this.endpointBatch)) .then(({ data: { pagination, diff_files: diffFiles } }) => { totalLoaded += diffFiles.length; - commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffFiles }); - commit(types.SET_BATCH_LOADING_STATE, 'loaded'); + this[types.SET_DIFF_DATA_BATCH]({ diff_files: diffFiles }); + this[types.SET_BATCH_LOADING_STATE]('loaded'); if (!scrolledVirtualScroller && !linkedFileLoading) { - const index = state.diffFiles.findIndex( + const index = this.diffFiles.findIndex( (f) => f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash), ); @@ -262,38 +263,38 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }, linkedFileLoadi } } - if (!isNoteLink && !state.currentDiffFileId) { - commit(types.SET_CURRENT_DIFF_FILE, diffFiles[0]?.file_hash); + if (!isNoteLink && !this.currentDiffFileId) { + this[types.SET_CURRENT_DIFF_FILE](diffFiles[0]?.file_hash); } if (isNoteLink) { - dispatch('setCurrentDiffFileIdFromNote', id.split('_').pop()); + this.setCurrentDiffFileIdFromNote(id.split('_').pop()); } if (totalLoaded === pagination.total_pages || pagination.total_pages === null) { - commit(types.SET_RETRIEVING_BATCHES, false); + this[types.SET_RETRIEVING_BATCHES](false); eventHub.$emit('doneLoadingBatches'); // We need to check that the currentDiffFileId points to a file that exists if ( - state.currentDiffFileId && - !state.diffFiles.some((f) => f.file_hash === state.currentDiffFileId) && + this.currentDiffFileId && + !this.diffFiles.some((f) => f.file_hash === this.currentDiffFileId) && !isNoteLink ) { - commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0].file_hash); + this[types.SET_CURRENT_DIFF_FILE](this.diffFiles[0].file_hash); } - if (state.diffFiles?.length) { + if (this.diffFiles?.length) { // eslint-disable-next-line promise/catch-or-return,promise/no-nesting import('~/code_navigation').then((m) => m.default({ - blobs: state.diffFiles + blobs: this.diffFiles .filter((f) => f.code_navigation_path) .map((f) => ({ path: f.new_path, codeNavigationPath: f.code_navigation_path, })), - definitionPathPrefix: state.definitionPathPrefix, + definitionPathPrefix: this.definitionPathPrefix, }), ); } @@ -315,24 +316,24 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }, linkedFileLoadi return null; }) .catch((error) => { - commit(types.SET_RETRIEVING_BATCHES, false); - commit(types.SET_BATCH_LOADING_STATE, 'error'); + this[types.SET_RETRIEVING_BATCHES](false); + this[types.SET_BATCH_LOADING_STATE]('error'); throw error; }); return getBatch(); -}; +} -export const fetchDiffFilesMeta = ({ commit, state }) => { +export function fetchDiffFilesMeta() { const urlParams = { view: 'inline', - w: state.showWhitespace ? '0' : '1', + w: this.showWhitespace ? '0' : '1', }; - commit(types.SET_LOADING, true); + this[types.SET_LOADING](true); return axios - .get(mergeUrlParams(urlParams, state.endpointMetadata)) + .get(mergeUrlParams(urlParams, this.endpointMetadata)) .then(({ data }) => { const strippedData = { ...data }; delete strippedData.diff_files; @@ -345,14 +346,14 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { }); } - commit(types.SET_LOADING, false); - commit(types.SET_MERGE_REQUEST_DIFFS, data.merge_request_diffs || []); - commit(types.SET_DIFF_METADATA, strippedData); + this[types.SET_LOADING](false); + this[types.SET_MERGE_REQUEST_DIFFS](data.merge_request_diffs || []); + this[types.SET_DIFF_METADATA](strippedData); eventHub.$emit(EVT_PERF_MARK_FILE_TREE_START); const { treeEntries, tree } = generateTreeList(data.diff_files); eventHub.$emit(EVT_PERF_MARK_FILE_TREE_END); - commit(types.SET_TREE_DATA, { + this[types.SET_TREE_DATA]({ treeEntries, tree: sortTree(tree), }); @@ -371,28 +372,28 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { throw error; } }); -}; +} -export function prefetchFileNeighbors({ getters, dispatch }) { - const { flatBlobsList: allBlobs, currentDiffIndex: currentIndex } = getters; +export function prefetchFileNeighbors() { + const { flatBlobsList: allBlobs, currentDiffIndex: currentIndex } = this; const previous = Math.max(currentIndex - 1, 0); const next = Math.min(allBlobs.length - 1, currentIndex + 1); - dispatch('prefetchSingleFile', allBlobs[next]); - dispatch('prefetchSingleFile', allBlobs[previous]); + this.prefetchSingleFile(allBlobs[next]); + this.prefetchSingleFile(allBlobs[previous]); } -export const fetchCoverageFiles = ({ commit, state }) => { +export function fetchCoverageFiles() { const coveragePoll = new Poll({ resource: { getCoverageReports: (endpoint) => axios.get(endpoint), }, - data: state.endpointCoverage, + data: this.endpointCoverage, method: 'getCoverageReports', successCallback: ({ status, data }) => { if (status === HTTP_STATUS_OK) { - commit(types.SET_COVERAGE_DATA, data); + this[types.SET_COVERAGE_DATA](data); coveragePoll.stop(); } @@ -404,35 +405,33 @@ export const fetchCoverageFiles = ({ commit, state }) => { }); coveragePoll.makeRequest(); -}; +} -export const setHighlightedRow = ({ commit }, { lineCode, event }) => { +export function setHighlightedRow({ lineCode, event }) { if (event && event.target.href) { event.preventDefault(); window.history.replaceState(null, undefined, removeParams(['file'], event.target.href)); } const fileHash = lineCode.split('_')[0]; - commit(types.SET_HIGHLIGHTED_ROW, lineCode); - commit(types.SET_CURRENT_DIFF_FILE, fileHash); + this[types.SET_HIGHLIGHTED_ROW](lineCode); + this[types.SET_CURRENT_DIFF_FILE](fileHash); handleLocationHash(); -}; +} // This is adding line discussions to the actual lines in the diff tree // once for parallel and once for inline mode -export const assignDiscussionsToDiff = ( - { commit, state, rootState, dispatch }, - discussions = rootState.notes.discussions, -) => { +export function assignDiscussionsToDiff(discussions) { + const targetDiscussions = discussions || useNotes().notes.discussions; const id = window?.location?.hash; const isNoteLink = id.indexOf('#note') === 0; - const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); + const diffPositionByLineCode = getDiffPositionByLineCode(this.diffFiles); const hash = getLocationHash(); - discussions + targetDiscussions .filter((discussion) => discussion.diff_discussion) .forEach((discussion) => { - commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { + this[types.SET_LINE_DISCUSSIONS_FOR_FILE]({ discussion, diffPositionByLineCode, hash, @@ -440,15 +439,15 @@ export const assignDiscussionsToDiff = ( }); if (isNoteLink) { - dispatch('setCurrentDiffFileIdFromNote', id.split('_').pop()); + this.setCurrentDiffFileIdFromNote(id.split('_').pop()); } Vue.nextTick(() => { eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED); }); -}; +} -export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { +export function removeDiscussionsFromDiff(removeDiscussion) { if (!removeDiscussion.diff_file) return; const { @@ -456,25 +455,25 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { line_code: lineCode, id, } = removeDiscussion; - commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode, id }); -}; + this[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE]({ fileHash, lineCode, id }); +} -export const toggleLineDiscussions = ({ commit }, options) => { - commit(types.TOGGLE_LINE_DISCUSSIONS, options); -}; +export function toggleLineDiscussions(options) { + this[types.TOGGLE_LINE_DISCUSSIONS](options); +} -export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { - const discussion = rootState.notes.discussions.find((d) => d.id === discussionId); +export function renderFileForDiscussionId(discussionId) { + const discussion = useNotes().notes.discussions.find((d) => d.id === discussionId); if (discussion && discussion.diff_file) { - const file = state.diffFiles.find((f) => f.file_hash === discussion.diff_file.file_hash); + const file = this.diffFiles.find((f) => f.file_hash === discussion.diff_file.file_hash); if (file) { if (file.viewer.automaticallyCollapsed) { notesEventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); scrollToElement(document.getElementById(file.file_hash)); } else if (file.viewer.manuallyCollapsed) { - commit(types.SET_FILE_COLLAPSED, { + this[types.SET_FILE_COLLAPSED]({ filePath: file.file_path, collapsed: false, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE, @@ -485,10 +484,10 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi } } } -}; +} -export const setDiffViewType = ({ commit }, diffViewType) => { - commit(types.SET_DIFF_VIEW_TYPE, diffViewType); +export function setDiffViewType(diffViewType) { + this[types.SET_DIFF_VIEW_TYPE](diffViewType); setCookie(DIFF_VIEW_COOKIE_NAME, diffViewType); const url = mergeUrlParams({ view: diffViewType }, window.location.href); @@ -500,10 +499,10 @@ export const setDiffViewType = ({ commit }, diffViewType) => { ? TRACKING_DIFF_VIEW_INLINE : TRACKING_DIFF_VIEW_PARALLEL, ]); -}; +} -export const showCommentForm = ({ commit }, { lineCode, fileHash }) => { - commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: true }); +export function showCommentForm({ lineCode, fileHash }) { + this[types.TOGGLE_LINE_HAS_FORM]({ lineCode, fileHash, hasForm: true }); // The comment form for diffs gets focussed differently due to the way the virtual scroller // works. If we focus the comment form on mount and the comment form gets removed and then @@ -526,13 +525,13 @@ export const showCommentForm = ({ commit }, { lineCode, fileHash }) => { window.scrollBy(0, Math.floor(Math.abs(overflowBottom)) + 150); } }); -}; +} -export const cancelCommentForm = ({ commit }, { lineCode, fileHash }) => { - commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: false }); -}; +export function cancelCommentForm({ lineCode, fileHash }) { + this[types.TOGGLE_LINE_HAS_FORM]({ lineCode, fileHash, hasForm: false }); +} -export const loadMoreLines = ({ commit }, options) => { +export function loadMoreLines(options) { const { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers } = options; params.from_merge_request = true; @@ -540,7 +539,7 @@ export const loadMoreLines = ({ commit }, options) => { return axios.get(endpoint, { params }).then((res) => { const contextLines = res.data || []; - commit(types.ADD_CONTEXT_LINES, { + this[types.ADD_CONTEXT_LINES]({ lineNumbers, contextLines, params, @@ -549,17 +548,17 @@ export const loadMoreLines = ({ commit }, options) => { nextLineNumbers, }); }); -}; +} -export const scrollToLineIfNeededInline = (_, line) => { +export function scrollToLineIfNeededInline(line) { const hash = getLocationHash(); if (hash && line.line_code === hash) { handleLocationHash(); } -}; +} -export const scrollToLineIfNeededParallel = (_, line) => { +export function scrollToLineIfNeededParallel(line) { const hash = getLocationHash(); if ( @@ -568,13 +567,13 @@ export const scrollToLineIfNeededParallel = (_, line) => { ) { handleLocationHash(); } -}; +} -export const loadCollapsedDiff = ({ commit, getters, state }, { file, params = {} }) => { - const versionPath = state.mergeRequestDiff?.version_path; +export function loadCollapsedDiff({ file, params = {} }) { + const versionPath = this.mergeRequestDiff?.version_path; const loadParams = { - commit_id: getters.commitId, - w: state.showWhitespace ? '0' : '1', + commit_id: this.commitId, + w: this.showWhitespace ? '0' : '1', ...params, }; @@ -586,23 +585,23 @@ export const loadCollapsedDiff = ({ commit, getters, state }, { file, params = { } return axios.get(file.load_collapsed_diff_url, { params: loadParams }).then((res) => { - commit(types.ADD_COLLAPSED_DIFFS, { + this[types.ADD_COLLAPSED_DIFFS]({ file, data: res.data, }); }); -}; +} /** * Toggles the file discussions after user clicked on the toggle discussions button. * @param {Object} discussion */ -export const toggleFileDiscussion = ({ commit }, discussion) => { - commit(types.TOGGLE_FILE_DISCUSSION_EXPAND, { discussion }); -}; +export function toggleFileDiscussion(discussion) { + this[types.TOGGLE_FILE_DISCUSSION_EXPAND]({ discussion }); +} -export const toggleFileDiscussionWrappers = ({ commit, getters }, diff) => { - const discussionWrappersExpanded = getters.diffHasExpandedDiscussions(diff); +export function toggleFileDiscussionWrappers(diff) { + const discussionWrappersExpanded = this.diffHasExpandedDiscussions(diff); const lineCodesWithDiscussions = new Set(); const lineHasDiscussion = (line) => Boolean(line?.discussions.length); const registerDiscussionLine = (line) => lineCodesWithDiscussions.add(line.line_code); @@ -611,7 +610,7 @@ export const toggleFileDiscussionWrappers = ({ commit, getters }, diff) => { if (lineCodesWithDiscussions.size) { Array.from(lineCodesWithDiscussions).forEach((lineCode) => { - commit(types.TOGGLE_LINE_DISCUSSIONS, { + this[types.TOGGLE_LINE_DISCUSSIONS]({ fileHash: diff.file_hash, expanded: !discussionWrappersExpanded, lineCode, @@ -622,20 +621,20 @@ export const toggleFileDiscussionWrappers = ({ commit, getters }, diff) => { if (diff.discussions.length) { diff.discussions.forEach((discussion) => { if (discussion.position?.position_type === FILE_DIFF_POSITION_TYPE) { - commit(types.TOGGLE_FILE_DISCUSSION_EXPAND, { + this[types.TOGGLE_FILE_DISCUSSION_EXPAND]({ discussion, expandedOnDiff: !discussionWrappersExpanded, }); } }); } -}; +} -export const saveDiffDiscussion = async ({ state, dispatch }, { note, formData }) => { +export async function saveDiffDiscussion({ note, formData }) { const postData = getNoteFormData({ - commit: state.commit, + commit: this.commit, note, - showWhitespace: state.showWhitespace, + showWhitespace: this.showWhitespace, ...formData, }); @@ -644,57 +643,58 @@ export const saveDiffDiscussion = async ({ state, dispatch }, { note, formData } return null; } - return dispatch('saveNote', postData, { root: true }) - .then((result) => dispatch('updateDiscussion', result.discussion, { root: true })) - .then((discussion) => dispatch('assignDiscussionsToDiff', [discussion])) - .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true })) - .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) + return useNotes() + .saveNote(postData) + .then((result) => useNotes().updateDiscussion(result.discussion)) + .then((discussion) => this.assignDiscussionsToDiff([discussion])) + .then(() => useNotes().updateResolvableDiscussionsCounts(null)) + .then(() => this.closeDiffFileCommentForm(formData.diffFile.file_hash)) .then(() => { if (formData.positionType === FILE_DIFF_POSITION_TYPE) { - dispatch('toggleFileCommentForm', formData.diffFile.file_path); + this.toggleFileCommentForm(formData.diffFile.file_path); } }); -}; +} -export const toggleTreeOpen = ({ commit }, path) => { - commit(types.TOGGLE_FOLDER_OPEN, path); -}; +export function toggleTreeOpen(path) { + this[types.TOGGLE_FOLDER_OPEN](path); +} -export const setCurrentFileHash = ({ commit }, hash) => { - commit(types.SET_CURRENT_DIFF_FILE, hash); -}; +export function setCurrentFileHash(hash) { + this[types.SET_CURRENT_DIFF_FILE](hash); +} -export const goToFile = ({ state, commit, dispatch, getters }, { path }) => { - if (!state.viewDiffsFileByFile) { - dispatch('scrollToFile', { path }); +export function goToFile({ path }) { + if (!this.viewDiffsFileByFile) { + this.scrollToFile({ path }); } else { - if (!state.treeEntries[path]) return; + if (!this.treeEntries[path]) return; - dispatch('unlinkFile'); + this.unlinkFile(); - const { fileHash } = state.treeEntries[path]; + const { fileHash } = this.treeEntries[path]; - commit(types.SET_CURRENT_DIFF_FILE, fileHash); + this[types.SET_CURRENT_DIFF_FILE](fileHash); const newUrl = new URL(window.location); newUrl.hash = fileHash; historyPushState(newUrl, { skipScrolling: true }); scrollToElement('.diff-files-holder', { duration: 0 }); - if (!getters.isTreePathLoaded(path)) { - dispatch('fetchFileByFile'); + if (!this.isTreePathLoaded(path)) { + this.fetchFileByFile(); } } -}; +} -export const scrollToFile = ({ state, commit, getters }, { path }) => { - if (!state.treeEntries[path]) return; +export function scrollToFile({ path }) { + if (!this.treeEntries[path]) return; - const { fileHash } = state.treeEntries[path]; + const { fileHash } = this.treeEntries[path]; - commit(types.SET_CURRENT_DIFF_FILE, fileHash); + this[types.SET_CURRENT_DIFF_FILE](fileHash); - if (getters.isVirtualScrollingEnabled) { + if (this.isVirtualScrollingEnabled) { eventHub.$emit('scrollToFileHash', fileHash); setTimeout(() => { @@ -707,36 +707,36 @@ export const scrollToFile = ({ state, commit, getters }, { path }) => { handleLocationHash(); }); } -}; +} -export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => { - commit(types.SET_SHOW_TREE_LIST, showTreeList); +export function setShowTreeList({ showTreeList, saving = true }) { + this[types.SET_SHOW_TREE_LIST](showTreeList); if (saving) { localStorage.setItem(MR_TREE_SHOW_KEY, showTreeList); } -}; +} -export const toggleTreeList = ({ state, commit }) => { - commit(types.SET_SHOW_TREE_LIST, !state.showTreeList); -}; +export function toggleTreeList() { + this[types.SET_SHOW_TREE_LIST](!this.showTreeList); +} -export const openDiffFileCommentForm = ({ commit, getters }, formData) => { - const form = getters.getCommentFormForDiffFile(formData.fileHash); +export function openDiffFileCommentForm(formData) { + const form = this.getCommentFormForDiffFile(formData.fileHash); if (form) { - commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData); + this[types.UPDATE_DIFF_FILE_COMMENT_FORM](formData); } else { - commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData); + this[types.OPEN_DIFF_FILE_COMMENT_FORM](formData); } -}; +} -export const closeDiffFileCommentForm = ({ commit }, fileHash) => { - commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash); -}; +export function closeDiffFileCommentForm(fileHash) { + this[types.CLOSE_DIFF_FILE_COMMENT_FORM](fileHash); +} -export const setRenderTreeList = ({ commit }, { renderTreeList, trackClick = true }) => { - commit(types.SET_RENDER_TREE_LIST, renderTreeList); +export function setRenderTreeList({ renderTreeList, trackClick = true }) { + this[types.SET_RENDER_TREE_LIST](renderTreeList); localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList); @@ -751,17 +751,19 @@ export const setRenderTreeList = ({ commit }, { renderTreeList, trackClick = tru queueRedisHllEvents(events); } -}; +} -export const setShowWhitespace = async ( - { state, commit }, - { url, showWhitespace, updateDatabase = true, trackClick = true }, -) => { +export async function setShowWhitespace({ + url, + showWhitespace, + updateDatabase = true, + trackClick = true, +}) { if (updateDatabase && Boolean(window.gon?.current_user_id)) { - await axios.put(url || state.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace }); + await axios.put(url || this.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace }); } - commit(types.SET_SHOW_WHITESPACE, showWhitespace); + this[types.SET_SHOW_WHITESPACE](showWhitespace); notesEventHub.$emit('refetchDiffData'); if (trackClick) { @@ -775,20 +777,20 @@ export const setShowWhitespace = async ( queueRedisHllEvents(events); } -}; +} -export const toggleFileFinder = ({ commit }, visible) => { - commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); -}; +export function toggleFileFinder(visible) { + this[types.TOGGLE_FILE_FINDER_VISIBLE](visible); +} -export const receiveFullDiffError = ({ commit }, filePath) => { - commit(types.RECEIVE_FULL_DIFF_ERROR, filePath); +export function receiveFullDiffError(filePath) { + this[types.RECEIVE_FULL_DIFF_ERROR](filePath); createAlert({ message: ERROR_LOADING_FULL_DIFF, }); -}; +} -export const setExpandedDiffLines = ({ commit }, { file, data }) => { +export function setExpandedDiffLines({ file, data }) { const expandedDiffLines = convertExpandLines({ diffLines: file[INLINE_DIFF_LINES_KEY], typeKey: TYPE_KEY, @@ -805,11 +807,11 @@ export const setExpandedDiffLines = ({ commit }, { file, data }) => { if (expandedDiffLines.length > MAX_RENDERING_DIFF_LINES) { let index = START_RENDERING_INDEX; - commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { + this[types.SET_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath: file.file_path, lines: expandedDiffLines.slice(0, index), }); - commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path); + this[types.TOGGLE_DIFF_FILE_RENDERING_MORE](file.file_path); const idleCb = (t) => { const startIndex = index; @@ -822,7 +824,7 @@ export const setExpandedDiffLines = ({ commit }, { file, data }) => { const line = expandedDiffLines[index]; if (line) { - commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line }); + this[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath: file.file_path, line }); index += 1; } } @@ -830,21 +832,21 @@ export const setExpandedDiffLines = ({ commit }, { file, data }) => { if (index !== expandedDiffLines.length) { idleCallback(idleCb); } else { - commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path); + this[types.TOGGLE_DIFF_FILE_RENDERING_MORE](file.file_path); } }; idleCallback(idleCb); } else { - commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { + this[types.SET_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath: file.file_path, lines: expandedDiffLines, }); } -}; +} -export const fetchFullDiff = ({ commit, dispatch }, file) => - axios +export function fetchFullDiff(file) { + return axios .get(file.context_lines_path, { params: { full: true, @@ -852,27 +854,28 @@ export const fetchFullDiff = ({ commit, dispatch }, file) => }, }) .then(({ data }) => { - commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath: file.file_path }); + this[types.RECEIVE_FULL_DIFF_SUCCESS]({ filePath: file.file_path }); - dispatch('setExpandedDiffLines', { file, data }); + this.setExpandedDiffLines({ file, data }); }) - .catch(() => dispatch('receiveFullDiffError', file.file_path)); + .catch(() => this.receiveFullDiffError(file.file_path)); +} -export const toggleFullDiff = ({ dispatch, commit, getters, state }, filePath) => { - const file = state.diffFiles.find((f) => f.file_path === filePath); +export function toggleFullDiff(filePath) { + const file = this.diffFiles.find((f) => f.file_path === filePath); - commit(types.REQUEST_FULL_DIFF, filePath); + this[types.REQUEST_FULL_DIFF](filePath); if (file.isShowingFullFile) { - dispatch('loadCollapsedDiff', { file }) - .then(() => dispatch('assignDiscussionsToDiff', getters.getDiffFileDiscussions(file))) - .catch(() => dispatch('receiveFullDiffError', filePath)); + this.loadCollapsedDiff({ file }) + .then(() => this.assignDiscussionsToDiff(this.getDiffFileDiscussions(file))) + .catch(() => this.receiveFullDiffError(filePath)); } else { - dispatch('fetchFullDiff', file); + this.fetchFullDiff(file); } -}; +} -export function switchToFullDiffFromRenamedFile({ commit }, { diffFile }) { +export function switchToFullDiffFromRenamedFile({ diffFile }) { return axios .get(diffFile.context_lines_path, { params: { @@ -890,7 +893,7 @@ export function switchToFullDiffFromRenamedFile({ commit }, { diffFile }) { }), ); - commit(types.SET_DIFF_FILE_VIEWER, { + this[types.SET_DIFF_FILE_VIEWER]({ filePath: diffFile.file_path, viewer: { ...diffFile.alternate_viewer, @@ -899,64 +902,65 @@ export function switchToFullDiffFromRenamedFile({ commit }, { diffFile }) { forceOpen: false, }, }); - commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); + this[types.SET_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath: diffFile.file_path, lines }); }); } -export const setFileCollapsedByUser = ({ commit }, { filePath, collapsed }) => { - commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_MANUAL_COLLAPSE }); -}; - -export const setFileCollapsedAutomatically = ({ commit }, { filePath, collapsed }) => { - commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE }); -}; - -export function setFileForcedOpen({ commit }, { filePath, forced }) { - commit(types.SET_FILE_FORCED_OPEN, { filePath, forced }); +export function setFileCollapsedByUser({ filePath, collapsed }) { + this[types.SET_FILE_COLLAPSED]({ filePath, collapsed, trigger: DIFF_FILE_MANUAL_COLLAPSE }); } -export const setSuggestPopoverDismissed = ({ commit, state }) => - axios - .post(state.dismissEndpoint, { +export function setFileCollapsedAutomatically({ filePath, collapsed }) { + this[types.SET_FILE_COLLAPSED]({ filePath, collapsed, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE }); +} + +export function setFileForcedOpen({ filePath, forced }) { + this[types.SET_FILE_FORCED_OPEN]({ filePath, forced }); +} + +export function setSuggestPopoverDismissed() { + return axios + .post(this.dismissEndpoint, { feature_name: 'suggest_popover_dismissed', }) .then(() => { - commit(types.SET_SHOW_SUGGEST_POPOVER); + this[types.SET_SHOW_SUGGEST_POPOVER](); }) .catch(() => { createAlert({ message: ERROR_DISMISSING_SUGESTION_POPOVER, }); }); +} -export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) { +export function changeCurrentCommit({ commitId }) { if (!commitId) { return Promise.reject(new Error('`commitId` is a required argument')); } - if (!state.commit) { + if (!this.commit) { return Promise.reject(new Error('`state` must already contain a valid `commit`')); // eslint-disable-line @gitlab/require-i18n-strings } // this is less than ideal, see: https://gitlab.com/gitlab-org/gitlab/-/issues/215421 - const commitRE = new RegExp(state.commit.id, 'g'); + const commitRE = new RegExp(this.commit.id, 'g'); - commit(types.SET_DIFF_FILES, []); - commit(types.SET_BASE_CONFIG, { - ...state, - endpoint: state.endpoint.replace(commitRE, commitId), - endpointBatch: state.endpointBatch.replace(commitRE, commitId), - endpointMetadata: state.endpointMetadata.replace(commitRE, commitId), + this[types.SET_DIFF_FILES]([]); + this[types.SET_BASE_CONFIG]({ + ...this.$state, + endpoint: this.endpoint.replace(commitRE, commitId), + endpointBatch: this.endpointBatch.replace(commitRE, commitId), + endpointMetadata: this.endpointMetadata.replace(commitRE, commitId), }); - return dispatch('fetchDiffFilesMeta'); + return this.fetchDiffFilesMeta(); } -export function moveToNeighboringCommit({ dispatch, state }, { direction }) { - const previousCommitId = state.commit?.prev_commit_id; - const nextCommitId = state.commit?.next_commit_id; +export function moveToNeighboringCommit({ direction }) { + const previousCommitId = this.commit?.prev_commit_id; + const nextCommitId = this.commit?.next_commit_id; const canMove = { - next: !state.isLoading && nextCommitId, - previous: !state.isLoading && previousCommitId, + next: !this.isLoading && nextCommitId, + previous: !this.isLoading && previousCommitId, }; let commitId; @@ -967,18 +971,18 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) { } if (commitId) { - dispatch('changeCurrentCommit', { commitId }); + this.changeCurrentCommit({ commitId }); } } -export const rereadNoteHash = ({ state, dispatch }) => { +export function rereadNoteHash() { const urlHash = window?.location?.hash; if (isUrlHashNoteLink(urlHash)) { - dispatch('setCurrentDiffFileIdFromNote', urlHash.split('_').pop()) + this.setCurrentDiffFileIdFromNote(urlHash.split('_').pop()) .then(() => { - if (state.viewDiffsFileByFile) { - dispatch('fetchFileByFile'); + if (this.viewDiffsFileByFile) { + this.fetchFileByFile(); } }) .catch(() => { @@ -987,35 +991,35 @@ export const rereadNoteHash = ({ state, dispatch }) => { }); }); } -}; +} -export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, noteId) => { - const note = rootGetters.notesById[noteId]; +export function setCurrentDiffFileIdFromNote(noteId) { + const note = useNotes().notesById[noteId]; if (!note) return; - const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash; + const fileHash = useNotes().getDiscussion(note.discussion_id).diff_file?.file_hash; - if (fileHash && getters.flatBlobsList.some((f) => f.fileHash === fileHash)) { - commit(types.SET_CURRENT_DIFF_FILE, fileHash); + if (fileHash && this.flatBlobsList.some((f) => f.fileHash === fileHash)) { + this[types.SET_CURRENT_DIFF_FILE](fileHash); } -}; +} -export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => { - dispatch('unlinkFile'); +export function navigateToDiffFileIndex(index) { + this.unlinkFile(); - const { fileHash } = getters.flatBlobsList[index]; + const { fileHash } = this.flatBlobsList[index]; document.location.hash = fileHash; - commit(types.SET_CURRENT_DIFF_FILE, fileHash); + this[types.SET_CURRENT_DIFF_FILE](fileHash); - if (state.viewDiffsFileByFile) { - dispatch('fetchFileByFile'); + if (this.viewDiffsFileByFile) { + this.fetchFileByFile(); } -}; +} -export const setFileByFile = ({ state, commit }, { fileByFile }) => { - commit(types.SET_FILE_BY_FILE, fileByFile); +export function setFileByFile({ fileByFile }) { + this[types.SET_FILE_BY_FILE](fileByFile); const events = [TRACKING_CLICK_SINGLE_FILE_SETTING]; @@ -1028,7 +1032,7 @@ export const setFileByFile = ({ state, commit }, { fileByFile }) => { queueRedisHllEvents(events); return axios - .put(state.endpointUpdateUser, { + .put(this.endpointUpdateUser, { view_diffs_file_by_file: fileByFile, }) .then(() => { @@ -1038,38 +1042,41 @@ export const setFileByFile = ({ state, commit }, { fileByFile }) => { // eventually handle errors appropriately. // console.warn('Saving the file-by-fil user preference failed.'); }); -}; +} -export function reviewFile({ commit, state }, { file, reviewed = true }) { +export function reviewFile({ file, reviewed = true }) { const { mrPath } = getDerivedMergeRequestInformation({ endpoint: file.load_collapsed_diff_url }); - const reviews = markFileReview(state.mrReviews, file, reviewed); + const reviews = markFileReview(this.mrReviews, file, reviewed); setReviewsForMergeRequest(mrPath, reviews); - commit(types.SET_DIFF_FILE_VIEWED, { id: file.file_hash, seen: reviewed }); - commit(types.SET_MR_FILE_REVIEWS, reviews); + this[types.SET_DIFF_FILE_VIEWED]({ id: file.file_hash, seen: reviewed }); + this[types.SET_MR_FILE_REVIEWS](reviews); } -export const disableVirtualScroller = ({ commit }) => commit(types.DISABLE_VIRTUAL_SCROLLING); +export function disableVirtualScroller() { + this[types.DISABLE_VIRTUAL_SCROLLING](); +} -export const toggleFileCommentForm = ({ state, commit }, filePath) => { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); +export function toggleFileCommentForm(filePath) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); if (isCollapsed(file)) { - commit(types.SET_FILE_COMMENT_FORM, { filePath, expanded: true }); + this[types.SET_FILE_COMMENT_FORM]({ filePath, expanded: true }); } else { - commit(types.TOGGLE_FILE_COMMENT_FORM, filePath); + this[types.TOGGLE_FILE_COMMENT_FORM](filePath); } - commit(types.SET_FILE_COLLAPSED, { filePath, collapsed: false }); -}; + this[types.SET_FILE_COLLAPSED]({ filePath, collapsed: false }); +} -export const addDraftToFile = ({ commit }, { filePath, draft }) => - commit(types.ADD_DRAFT_TO_FILE, { filePath, draft }); +export function addDraftToFile({ filePath, draft }) { + return this[types.ADD_DRAFT_TO_FILE]({ filePath, draft }); +} -export const fetchLinkedFile = ({ state, commit }, linkedFileUrl) => { +export function fetchLinkedFile(linkedFileUrl) { const isNoteLink = isUrlHashNoteLink(window?.location?.hash); - commit(types.SET_BATCH_LOADING_STATE, 'loading'); - commit(types.SET_RETRIEVING_BATCHES, true); + this[types.SET_BATCH_LOADING_STATE]('loading'); + this[types.SET_RETRIEVING_BATCHES](true); return axios .get(linkedFileUrl) @@ -1079,14 +1086,14 @@ export const fetchLinkedFile = ({ state, commit }, linkedFileUrl) => { // we must store linked file in the `diffs`, otherwise collapsing and commenting on a file won't work // once the same file arrives in a file batch we must only update its' position // we also must not update file's position since it's loaded out of order - commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files, updatePosition: false }); - commit(types.SET_LINKED_FILE_HASH, file_hash); + this[types.SET_DIFF_DATA_BATCH]({ diff_files: diffData.diff_files, updatePosition: false }); + this[types.SET_LINKED_FILE_HASH](file_hash); - if (!isNoteLink && !state.currentDiffFileId) { - commit(types.SET_CURRENT_DIFF_FILE, file_hash); + if (!isNoteLink && !this.currentDiffFileId) { + this[types.SET_CURRENT_DIFF_FILE](file_hash); } - commit(types.SET_BATCH_LOADING_STATE, 'loaded'); + this[types.SET_BATCH_LOADING_STATE]('loaded'); setTimeout(() => { handleLocationHash(); @@ -1095,23 +1102,23 @@ export const fetchLinkedFile = ({ state, commit }, linkedFileUrl) => { eventHub.$emit('diffFilesModified'); }) .catch((error) => { - commit(types.SET_BATCH_LOADING_STATE, 'error'); + this[types.SET_BATCH_LOADING_STATE]('error'); throw error; }) .finally(() => { - commit(types.SET_RETRIEVING_BATCHES, false); + this[types.SET_RETRIEVING_BATCHES](false); }); -}; +} -export const unlinkFile = ({ getters, commit }) => { - if (!getters.linkedFile) return; - commit(types.SET_LINKED_FILE_HASH, null); +export function unlinkFile() { + if (!this.linkedFile) return; + this[types.SET_LINKED_FILE_HASH](null); const newUrl = new URL(window.location); newUrl.searchParams.delete('file'); newUrl.hash = ''; window.history.replaceState(null, undefined, newUrl); -}; +} -export const toggleAllDiffDiscussions = ({ commit, getters }) => { - commit(types.SET_EXPAND_ALL_DIFF_DISCUSSIONS, !getters.allDiffDiscussionsExpanded); -}; +export function toggleAllDiffDiscussions() { + this[types.SET_EXPAND_ALL_DIFF_DISCUSSIONS](!this.allDiffDiscussionsExpanded); +} diff --git a/app/assets/javascripts/diffs/stores/legacy_diffs/getters.js b/app/assets/javascripts/diffs/stores/legacy_diffs/getters.js index 123fdd56bb3..425dc5deddd 100644 --- a/app/assets/javascripts/diffs/stores/legacy_diffs/getters.js +++ b/app/assets/javascripts/diffs/stores/legacy_diffs/getters.js @@ -7,77 +7,93 @@ import { INLINE_DIFF_LINES_KEY, DIFF_COMPARE_BASE_VERSION_INDEX, DIFF_COMPARE_HEAD_VERSION_INDEX, -} from '../../constants'; +} from '~/diffs/constants'; +import { useNotes } from '~/notes/store/legacy_notes'; +import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes'; import { computeSuggestionCommitMessage } from '../../utils/suggestions'; import { parallelizeDiffLines } from '../../store/utils'; -export const isParallelView = (state) => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; +export function isParallelView() { + return this.diffViewType === PARALLEL_DIFF_VIEW_TYPE; +} -export const isInlineView = (state) => state.diffViewType === INLINE_DIFF_VIEW_TYPE; +export function isInlineView() { + return this.diffViewType === INLINE_DIFF_VIEW_TYPE; +} -export const whichCollapsedTypes = (state) => { - const automatic = state.diffFiles.some((file) => file.viewer?.automaticallyCollapsed); - const manual = state.diffFiles.some((file) => file.viewer?.manuallyCollapsed); +export function whichCollapsedTypes() { + const automatic = this.diffFiles.some((file) => file.viewer?.automaticallyCollapsed); + const manual = this.diffFiles.some((file) => file.viewer?.manuallyCollapsed); return { any: automatic || manual, automatic, manual, }; -}; +} -export const commitId = (state) => (state.commit && state.commit.id ? state.commit.id : null); +export function commitId() { + return this.commit && this.commit.id ? this.commit.id : null; +} /** * Checks if the diff has all discussions expanded * @param {Object} diff * @returns {Boolean} */ -export const diffHasAllExpandedDiscussions = (state, getters) => (diff) => { - const discussions = getters.getDiffFileDiscussions(diff); +export function diffHasAllExpandedDiscussions() { + return (diff) => { + const discussions = this.getDiffFileDiscussions(diff); - return ( - (discussions && discussions.length && discussions.every((discussion) => discussion.expanded)) || - false - ); -}; + return ( + (discussions && + discussions.length && + discussions.every((discussion) => discussion.expanded)) || + false + ); + }; +} /** * Checks if the diff has all discussions collapsed * @param {Object} diff * @returns {Boolean} */ -export const diffHasAllCollapsedDiscussions = (state, getters) => (diff) => { - const discussions = getters.getDiffFileDiscussions(diff); +export function diffHasAllCollapsedDiscussions() { + return (diff) => { + const discussions = this.getDiffFileDiscussions(diff); - return ( - (discussions && - discussions.length && - discussions.every((discussion) => !discussion.expanded)) || - false - ); -}; + return ( + (discussions && + discussions.length && + discussions.every((discussion) => !discussion.expanded)) || + false + ); + }; +} /** * Checks if the diff has any open discussions * @param {Object} diff * @returns {Boolean} */ -export const diffHasExpandedDiscussions = () => (diff) => { - const diffLineDiscussionsExpanded = diff[INLINE_DIFF_LINES_KEY].filter( - (l) => l.discussions.length >= 1, - ).some((l) => l.discussionsExpanded); - const diffFileDiscussionsExpanded = diff.discussions?.some((d) => d.expandedOnDiff); +export function diffHasExpandedDiscussions() { + return (diff) => { + const diffLineDiscussionsExpanded = diff[INLINE_DIFF_LINES_KEY].filter( + (l) => l.discussions.length >= 1, + ).some((l) => l.discussionsExpanded); + const diffFileDiscussionsExpanded = diff.discussions?.some((d) => d.expandedOnDiff); - return diffFileDiscussionsExpanded || diffLineDiscussionsExpanded; -}; + return diffFileDiscussionsExpanded || diffLineDiscussionsExpanded; + }; +} /** * Checks if every diff has every discussion open * @returns {Boolean} */ -export const allDiffDiscussionsExpanded = (state) => { - return state.diffFiles.every((diff) => { +export function allDiffDiscussionsExpanded() { + return this.diffFiles.every((diff) => { const highlightedLines = diff[INLINE_DIFF_LINES_KEY]; if (highlightedLines.length) { return highlightedLines @@ -89,45 +105,51 @@ export const allDiffDiscussionsExpanded = (state) => { } return true; }); -}; +} /** * Checks if the diff has any discussion * @param {Boolean} diff * @returns {Boolean} */ -export const diffHasDiscussions = () => (diff) => { - return ( - diff.discussions?.length >= 1 || - diff[INLINE_DIFF_LINES_KEY].some((l) => l.discussions.length >= 1) - ); -}; +export function diffHasDiscussions() { + return (diff) => { + return ( + diff.discussions?.length >= 1 || + diff[INLINE_DIFF_LINES_KEY].some((l) => l.discussions.length >= 1) + ); + }; +} /** * Returns an array with the discussions of the given diff * @param {Object} diff * @returns {Array} */ -// eslint-disable-next-line max-params -export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => (diff) => - rootGetters.discussions.filter( - (discussion) => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash, - ) || []; +export function getDiffFileDiscussions() { + return (diff) => + useNotes().discussions.filter( + (discussion) => + discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash, + ) || []; +} -export const getDiffFileByHash = (state) => (fileHash) => - state.diffFiles.find((file) => file.file_hash === fileHash); +export function getDiffFileByHash() { + return (fileHash) => this.diffFiles.find((file) => file.file_hash === fileHash); +} -export function isTreePathLoaded(state) { +export function isTreePathLoaded() { return (path) => { - return Boolean(state.treeEntries[path]?.diffLoaded); + return Boolean(this.treeEntries[path]?.diffLoaded); }; } -export const flatBlobsList = (state) => - Object.values(state.treeEntries).filter((f) => f.type === 'blob'); +export function flatBlobsList() { + return Object.values(this.treeEntries).filter((f) => f.type === 'blob'); +} -export const allBlobs = (state, getters) => - getters.flatBlobsList.reduce((acc, file) => { +export function allBlobs() { + return this.flatBlobsList.reduce((acc, file) => { const { parentPath } = file; if (parentPath && !acc.some((f) => f.path === parentPath)) { @@ -142,9 +164,11 @@ export const allBlobs = (state, getters) => return acc; }, []); +} -export const getCommentFormForDiffFile = (state) => (fileHash) => - state.commentForms.find((form) => form.fileHash === fileHash); +export function getCommentFormForDiffFile() { + return (fileHash) => this.commentForms.find((form) => form.fileHash === fileHash); +} /** * Returns the test coverage hits for a specific line of a given file @@ -152,114 +176,131 @@ export const getCommentFormForDiffFile = (state) => (fileHash) => * @param {number} line * @returns {number} */ -export const fileLineCoverage = (state) => (file, line) => { - if (!state.coverageFiles.files) return {}; - const fileCoverage = state.coverageFiles.files[file]; - if (!fileCoverage) return {}; - const lineCoverage = fileCoverage[String(line)]; +export function fileLineCoverage() { + return (file, line) => { + if (!this.coverageFiles.files) return {}; + const fileCoverage = this.coverageFiles.files[file]; + if (!fileCoverage) return {}; + const lineCoverage = fileCoverage[String(line)]; - if (lineCoverage === 0) { - return { text: __('No test coverage'), class: 'no-coverage' }; - } - if (lineCoverage >= 0) { - return { - text: n__('Test coverage: %d hit', 'Test coverage: %d hits', lineCoverage), - class: 'coverage', - }; - } - return {}; -}; + if (lineCoverage === 0) { + return { text: __('No test coverage'), class: 'no-coverage' }; + } + if (lineCoverage >= 0) { + return { + text: n__('Test coverage: %d hit', 'Test coverage: %d hits', lineCoverage), + class: 'coverage', + }; + } + return {}; + }; +} /** * Returns index of a currently selected diff in diffFiles * @returns {number} */ -export const currentDiffIndex = (state) => - Math.max( +export function currentDiffIndex() { + return Math.max( 0, - flatBlobsList(state).findIndex((diff) => diff.fileHash === state.currentDiffFileId), + this.flatBlobsList.findIndex((diff) => diff.fileHash === this.currentDiffFileId), ); +} -export const diffLines = (state) => (file) => { - return parallelizeDiffLines( - file.highlighted_diff_lines || [], - state.diffViewType === INLINE_DIFF_VIEW_TYPE, - ); -}; +export function diffLines() { + return (file) => { + return parallelizeDiffLines( + file.highlighted_diff_lines || [], + this.diffViewType === INLINE_DIFF_VIEW_TYPE, + ); + }; +} -export function suggestionCommitMessage(state, _, rootState) { - return (values = {}) => - computeSuggestionCommitMessage({ - message: state.defaultSuggestionCommitMessage, +export function suggestionCommitMessage() { + return (values = {}) => { + const { mrMetadata } = useMrNotes().page; + return computeSuggestionCommitMessage({ + message: this.defaultSuggestionCommitMessage, values: { - branch_name: rootState.page.mrMetadata.branch_name, - project_path: rootState.page.mrMetadata.project_path, - project_name: rootState.page.mrMetadata.project_name, - username: rootState.page.mrMetadata.username, - user_full_name: rootState.page.mrMetadata.user_full_name, + branch_name: mrMetadata.branch_name, + project_path: mrMetadata.project_path, + project_name: mrMetadata.project_name, + username: mrMetadata.username, + user_full_name: mrMetadata.user_full_name, ...values, }, }); + }; } -export const isVirtualScrollingEnabled = (state) => { - if (state.disableVirtualScroller || getParameterValues('virtual_scrolling')[0] === 'false') { +export function isVirtualScrollingEnabled() { + if (this.disableVirtualScroller || getParameterValues('virtual_scrolling')[0] === 'false') { return false; } - return !state.viewDiffsFileByFile; -}; + return !this.viewDiffsFileByFile; +} -export const isBatchLoading = (state) => state.batchLoadingState === 'loading'; -export const isBatchLoadingError = (state) => state.batchLoadingState === 'error'; +export function isBatchLoading() { + return this.batchLoadingState === 'loading'; +} +export function isBatchLoadingError() { + return this.batchLoadingState === 'error'; +} -export const diffFiles = (state, getters) => { - const { linkedFile } = getters; - if (linkedFile) { - const diffs = state.diffFiles.slice(0); - diffs.splice(diffs.indexOf(linkedFile), 1); - return [linkedFile, ...diffs]; +export function diffFilesFiltered() { + const { linkedFile: file } = this; + if (file) { + const diffs = this.diffFiles.slice(0); + diffs.splice(diffs.indexOf(file), 1); + return [file, ...diffs]; } - return state.diffFiles; -}; + return this.diffFiles; +} -export const linkedFile = (state) => { - if (!state.linkedFileHash) return null; - return state.diffFiles.find((file) => file.file_hash === state.linkedFileHash); -}; +export function linkedFile() { + if (!this.linkedFileHash) return null; + return this.diffFiles.find((file) => file.file_hash === this.linkedFileHash); +} -export const selectedTargetIndex = (state) => - state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX; +export function selectedTargetIndex() { + return this.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX; +} -export const selectedSourceIndex = (state) => state.mergeRequestDiff.version_index; +export function selectedSourceIndex() { + if (!this.mergeRequestDiff) return undefined; + return this.mergeRequestDiff.version_index; +} -export const selectedContextCommitsDiffs = (state) => - state.contextCommitsDiff && state.contextCommitsDiff.showing_context_commits_diff; +export function selectedContextCommitsDiff() { + return this.contextCommitsDiff && this.contextCommitsDiff.showing_context_commits_diff; +} -export const diffCompareDropdownTargetVersions = (state, getters) => { +export function diffCompareDropdownTargetVersions() { + if (!this.mergeRequestDiff) return []; // startVersion only exists if the user has selected a version other // than "base" so if startVersion is null then base must be selected const diffHeadParam = getParameterByName('diff_head'); const diffHead = parseBoolean(diffHeadParam) || !diffHeadParam; - const isBaseSelected = !state.startVersion; - const isHeadSelected = !state.startVersion && diffHead; + const isBaseSelected = !this.startVersion; + const isHeadSelected = !this.startVersion && diffHead; let baseVersion = null; - if (!state.mergeRequestDiff.head_version_path) { + if (!this.mergeRequestDiff.head_version_path) { baseVersion = { - versionName: state.targetBranchName, + versionName: this.targetBranchName, version_index: DIFF_COMPARE_BASE_VERSION_INDEX, - href: state.mergeRequestDiff.base_version_path, + href: this.mergeRequestDiff.base_version_path, isBase: true, selected: isBaseSelected, }; } const headVersion = { - versionName: state.targetBranchName, + versionName: this.targetBranchName, version_index: DIFF_COMPARE_HEAD_VERSION_INDEX, - href: state.mergeRequestDiff.head_version_path, + href: this.mergeRequestDiff.head_version_path, isHead: true, selected: isHeadSelected, }; @@ -268,21 +309,21 @@ export const diffCompareDropdownTargetVersions = (state, getters) => { return { href: v.compare_path, versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }), - selected: v.version_index === getters.selectedTargetIndex, + selected: v.version_index === this.selectedTargetIndex, ...v, }; }; return [ - ...state.mergeRequestDiffs.slice(1).map(formatVersion), + ...this.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, - state.mergeRequestDiff.head_version_path && headVersion, + this.mergeRequestDiff.head_version_path && headVersion, ].filter((a) => a); -}; +} -export const diffCompareDropdownSourceVersions = (state, getters) => { +export function diffCompareDropdownSourceVersions() { // Appended properties here are to make the compare_dropdown_layout easier to reason about - const versions = state.mergeRequestDiffs.map((v, i) => { + const versions = this.mergeRequestDiffs.map((v, i) => { const isLatestVersion = i === 0; return { @@ -293,20 +334,19 @@ export const diffCompareDropdownSourceVersions = (state, getters) => { versionName: isLatestVersion ? __('latest version') : sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }), - selected: - v.version_index === getters.selectedSourceIndex && !getters.selectedContextCommitsDiffs, + selected: v.version_index === this.selectedSourceIndex && !this.selectedContextCommitsDiffs, }; }); - const { contextCommitsDiff } = state; + const { contextCommitsDiff } = this; if (contextCommitsDiff) { versions.push({ href: contextCommitsDiff.diffs_path, commitsText: n__(`%d commit`, `%d commits`, contextCommitsDiff.commits_count), versionName: __('previously merged commits'), - selected: getters.selectedContextCommitsDiffs, - addDivider: state.mergeRequestDiffs.length > 0, + selected: this.selectedContextCommitsDiffs, + addDivider: this.mergeRequestDiffs.length > 0, }); } return versions; -}; +} diff --git a/app/assets/javascripts/diffs/stores/legacy_diffs/index.js b/app/assets/javascripts/diffs/stores/legacy_diffs/index.js index 7baef00273c..45b1dd047c7 100644 --- a/app/assets/javascripts/diffs/stores/legacy_diffs/index.js +++ b/app/assets/javascripts/diffs/stores/legacy_diffs/index.js @@ -47,7 +47,7 @@ export const useLegacyDiffs = defineStore('legacyDiffs', { mrReviews: {}, latestDiff: true, disableVirtualScroller: false, - pinnedFileHash: null, + linkedFileHash: null, }; }, actions: { diff --git a/app/assets/javascripts/diffs/stores/legacy_diffs/mutations.js b/app/assets/javascripts/diffs/stores/legacy_diffs/mutations.js index de963004b07..d56b24dd9d6 100644 --- a/app/assets/javascripts/diffs/stores/legacy_diffs/mutations.js +++ b/app/assets/javascripts/diffs/stores/legacy_diffs/mutations.js @@ -18,12 +18,8 @@ import { markTreeEntriesLoaded, } from '../../store/utils'; -function updateDiffFilesInState(state, files) { - return Object.assign(state, { diffFiles: files }); -} - export default { - [types.SET_BASE_CONFIG](state, options) { + [types.SET_BASE_CONFIG](options) { const { endpoint, endpointMetadata, @@ -40,7 +36,7 @@ export default { diffViewType, perPage, } = options; - Object.assign(state, { + Object.assign(this, { endpoint, endpointMetadata, endpointBatch, @@ -58,79 +54,79 @@ export default { }); }, - [types.SET_LOADING](state, isLoading) { - Object.assign(state, { isLoading }); + [types.SET_LOADING](isLoading) { + Object.assign(this, { isLoading }); }, - [types.SET_BATCH_LOADING_STATE](state, batchLoadingState) { - Object.assign(state, { batchLoadingState }); + [types.SET_BATCH_LOADING_STATE](batchLoadingState) { + Object.assign(this, { batchLoadingState }); }, - [types.SET_RETRIEVING_BATCHES](state, retrievingBatches) { - Object.assign(state, { retrievingBatches }); + [types.SET_RETRIEVING_BATCHES](retrievingBatches) { + Object.assign(this, { retrievingBatches }); }, - [types.SET_DIFF_FILES](state, files) { - updateDiffFilesInState(state, files); + [types.SET_DIFF_FILES](files) { + return Object.assign(this, { diffFiles: files }); }, - [types.SET_DIFF_METADATA](state, data) { - Object.assign(state, { + [types.SET_DIFF_METADATA](data) { + Object.assign(this, { ...convertObjectPropsToCamelCase(data), }); }, - [types.SET_DIFF_DATA_BATCH](state, { diff_files: diffFiles, updatePosition = true }) { - Object.assign(state, { + [types.SET_DIFF_DATA_BATCH]({ diff_files: diffFiles, updatePosition = true }) { + Object.assign(this, { diffFiles: prepareDiffData({ diff: { diff_files: diffFiles }, - priorFiles: state.diffFiles, + priorFiles: this.diffFiles, // when a linked file is added to diffs its position may be incorrect since it's loaded out of order // we need to ensure when we load it in batched request it updates it position updatePosition, }), treeEntries: markTreeEntriesLoaded({ - priorEntries: state.treeEntries, + priorEntries: this.treeEntries, loadedFiles: diffFiles, }), }); }, - [types.SET_DIFF_TREE_ENTRY](state, diffFile) { - Object.assign(state, { + [types.SET_DIFF_TREE_ENTRY](diffFile) { + Object.assign(this, { treeEntries: markTreeEntriesLoaded({ - priorEntries: state.treeEntries, + priorEntries: this.treeEntries, loadedFiles: [diffFile], }), }); }, - [types.SET_COVERAGE_DATA](state, coverageFiles) { - Object.assign(state, { coverageFiles, coverageLoaded: true }); + [types.SET_COVERAGE_DATA](coverageFiles) { + Object.assign(this, { coverageFiles, coverageLoaded: true }); }, - [types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) { - Object.assign(state, { + [types.SET_MERGE_REQUEST_DIFFS](mergeRequestDiffs) { + Object.assign(this, { mergeRequestDiffs, }); }, - [types.SET_DIFF_VIEW_TYPE](state, diffViewType) { - Object.assign(state, { diffViewType }); + [types.SET_DIFF_VIEW_TYPE](diffViewType) { + Object.assign(this, { diffViewType }); }, - [types.TOGGLE_LINE_HAS_FORM](state, { lineCode, fileHash, hasForm }) { - const diffFile = state.diffFiles.find((f) => f.file_hash === fileHash); + [types.TOGGLE_LINE_HAS_FORM]({ lineCode, fileHash, hasForm }) { + const diffFile = this.diffFiles.find((f) => f.file_hash === fileHash); if (!diffFile) return; diffFile[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === lineCode).hasForm = hasForm; }, - [types.ADD_CONTEXT_LINES](state, options) { + [types.ADD_CONTEXT_LINES](options) { const { lineNumbers, contextLines, fileHash, isExpandDown, nextLineNumbers } = options; const { bottom } = options.params; - const diffFile = findDiffFile(state.diffFiles, fileHash); + const diffFile = findDiffFile(this.diffFiles, fileHash); removeMatchLine(diffFile, lineNumbers, bottom); @@ -163,18 +159,18 @@ export default { }); }, - [types.ADD_COLLAPSED_DIFFS](state, { file, data }) { + [types.ADD_COLLAPSED_DIFFS]({ file, data }) { const files = prepareDiffData({ diff: data }); const [newFileData] = files.filter((f) => f.file_hash === file.file_hash); - const selectedFile = state.diffFiles.find((f) => f.file_hash === file.file_hash); + const selectedFile = this.diffFiles.find((f) => f.file_hash === file.file_hash); Object.assign(selectedFile, { ...newFileData, whitespaceOnlyChange: selectedFile.whitespaceOnlyChange, }); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) { - const { latestDiff } = state; + [types.SET_LINE_DISCUSSIONS_FOR_FILE]({ discussion, diffPositionByLineCode, hash }) { + const { latestDiff } = this; const originalStartLineCode = discussion.original_position?.line_range?.start?.line_code; const positionType = discussion.position?.position_type; const discussionLineCodes = [ @@ -205,7 +201,7 @@ export default { const addDiscussion = (discussions) => discussions.filter(({ id }) => discussion.id !== id).concat(discussion); - const file = state.diffFiles.find((diff) => diff.file_hash === fileHash); + const file = this.diffFiles.find((diff) => diff.file_hash === fileHash); // a file batch might not be loaded yet when we try to add a discussion if (!file) return; const diffLines = file[INLINE_DIFF_LINES_KEY]; @@ -227,19 +223,19 @@ export default { } }, - [types.TOGGLE_FILE_DISCUSSION_EXPAND]( - state, - { discussion, expandedOnDiff = !discussion.expandedOnDiff }, - ) { + [types.TOGGLE_FILE_DISCUSSION_EXPAND]({ + discussion, + expandedOnDiff = !discussion.expandedOnDiff, + }) { Object.assign(discussion, { expandedOnDiff }); const fileHash = discussion.diff_file.file_hash; - const diff = state.diffFiles.find((f) => f.file_hash === fileHash); + const diff = this.diffFiles.find((f) => f.file_hash === fileHash); // trigger Vue reactivity Object.assign(diff, { discussions: [...diff.discussions] }); }, - [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { - const selectedFile = state.diffFiles.find((f) => f.file_hash === fileHash); + [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE]({ fileHash, lineCode }) { + const selectedFile = this.diffFiles.find((f) => f.file_hash === fileHash); if (selectedFile) { updateLineInFile(selectedFile, lineCode, (line) => Object.assign(line, { @@ -255,17 +251,17 @@ export default { } }, - [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) { - const selectedFile = state.diffFiles.find((f) => f.file_hash === fileHash); + [types.TOGGLE_LINE_DISCUSSIONS]({ fileHash, lineCode, expanded }) { + const selectedFile = this.diffFiles.find((f) => f.file_hash === fileHash); updateLineInFile(selectedFile, lineCode, (line) => { Object.assign(line, { discussionsExpanded: expanded }); }); }, - [types.SET_EXPAND_ALL_DIFF_DISCUSSIONS](state, expanded) { + [types.SET_EXPAND_ALL_DIFF_DISCUSSIONS](expanded) { const lineHasDiscussion = (line) => Boolean(line.discussions?.length); - state.diffFiles.forEach((file) => { + this.diffFiles.forEach((file) => { const highlightedLines = file[INLINE_DIFF_LINES_KEY]; if (highlightedLines.length) { const discussionLines = highlightedLines.filter(lineHasDiscussion); @@ -284,33 +280,33 @@ export default { }); }, - [types.TOGGLE_FOLDER_OPEN](state, path) { - state.treeEntries[path].opened = !state.treeEntries[path].opened; + [types.TOGGLE_FOLDER_OPEN](path) { + this.treeEntries[path].opened = !this.treeEntries[path].opened; }, - [types.TREE_ENTRY_DIFF_LOADING](state, { path, loading = true }) { - state.treeEntries[path].diffLoading = loading; + [types.TREE_ENTRY_DIFF_LOADING]({ path, loading = true }) { + this.treeEntries[path].diffLoading = loading; }, - [types.SET_SHOW_TREE_LIST](state, showTreeList) { - state.showTreeList = showTreeList; + [types.SET_SHOW_TREE_LIST](showTreeList) { + this.showTreeList = showTreeList; }, - [types.SET_CURRENT_DIFF_FILE](state, fileId) { - state.currentDiffFileId = fileId; + [types.SET_CURRENT_DIFF_FILE](fileId) { + this.currentDiffFileId = fileId; }, - [types.SET_DIFF_FILE_VIEWED](state, { id, seen }) { - state.viewedDiffFileIds = { - ...state.viewedDiffFileIds, + [types.SET_DIFF_FILE_VIEWED]({ id, seen }) { + this.viewedDiffFileIds = { + ...this.viewedDiffFileIds, [id]: seen, }; }, - [types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) { - state.commentForms.push({ + [types.OPEN_DIFF_FILE_COMMENT_FORM](formData) { + this.commentForms.push({ ...formData, }); }, - [types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) { + [types.UPDATE_DIFF_FILE_COMMENT_FORM](formData) { const { fileHash } = formData; - state.commentForms = state.commentForms.map((form) => { + this.commentForms = this.commentForms.map((form) => { if (form.fileHash === fileHash) { return { ...formData, @@ -320,48 +316,45 @@ export default { return form; }); }, - [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) { - state.commentForms = state.commentForms.filter((form) => form.fileHash !== fileHash); + [types.CLOSE_DIFF_FILE_COMMENT_FORM](fileHash) { + this.commentForms = this.commentForms.filter((form) => form.fileHash !== fileHash); }, - [types.SET_HIGHLIGHTED_ROW](state, lineCode) { - state.highlightedRow = lineCode; + [types.SET_HIGHLIGHTED_ROW](lineCode) { + this.highlightedRow = lineCode; }, - [types.SET_TREE_DATA](state, { treeEntries, tree }) { - state.treeEntries = treeEntries; - state.tree = tree; - state.isTreeLoaded = true; + [types.SET_TREE_DATA]({ treeEntries, tree }) { + this.treeEntries = treeEntries; + this.tree = tree; + this.isTreeLoaded = true; }, - [types.SET_RENDER_TREE_LIST](state, renderTreeList) { - state.renderTreeList = renderTreeList; + [types.SET_RENDER_TREE_LIST](renderTreeList) { + this.renderTreeList = renderTreeList; }, - [types.SET_SHOW_WHITESPACE](state, showWhitespace) { - state.showWhitespace = showWhitespace; - state.diffFiles = []; + [types.SET_SHOW_WHITESPACE](showWhitespace) { + this.showWhitespace = showWhitespace; + this.diffFiles = []; }, - [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) { - state.fileFinderVisible = visible; + [types.TOGGLE_FILE_FINDER_VISIBLE](visible) { + this.fileFinderVisible = visible; }, - [types.REQUEST_FULL_DIFF](state, filePath) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.REQUEST_FULL_DIFF](filePath) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.isLoadingFullFile = true; }, - [types.RECEIVE_FULL_DIFF_ERROR](state, filePath) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.RECEIVE_FULL_DIFF_ERROR](filePath) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.isLoadingFullFile = false; }, - [types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath }) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.RECEIVE_FULL_DIFF_SUCCESS]({ filePath }) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.isShowingFullFile = true; file.isLoadingFullFile = false; }, - [types.SET_FILE_COLLAPSED]( - state, - { filePath, collapsed, trigger = DIFF_FILE_AUTOMATIC_COLLAPSE }, - ) { - const file = state.diffFiles.find((f) => f.file_path === filePath); + [types.SET_FILE_COLLAPSED]({ filePath, collapsed, trigger = DIFF_FILE_AUTOMATIC_COLLAPSE }) { + const file = this.diffFiles.find((f) => f.file_path === filePath); if (file && file.viewer) { if (trigger === DIFF_FILE_MANUAL_COLLAPSE) { @@ -373,58 +366,58 @@ export default { } } }, - [types.SET_FILE_FORCED_OPEN](state, { filePath, forced = true }) { - const file = state.diffFiles.find((f) => f.file_path === filePath); + [types.SET_FILE_FORCED_OPEN]({ filePath, forced = true }) { + const file = this.diffFiles.find((f) => f.file_path === filePath); file.viewer.forceOpen = forced; }, - [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { - const file = state.diffFiles.find((f) => f.file_path === filePath); + [types.SET_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath, lines }) { + const file = this.diffFiles.find((f) => f.file_path === filePath); - file[INLINE_DIFF_LINES_KEY] = lines; + file[INLINE_DIFF_LINES_KEY] = [...lines]; }, - [types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) { - const file = state.diffFiles.find((f) => f.file_path === filePath); + [types.ADD_CURRENT_VIEW_DIFF_FILE_LINES]({ filePath, line }) { + const file = this.diffFiles.find((f) => f.file_path === filePath); file[INLINE_DIFF_LINES_KEY].push(line); }, - [types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) { - const file = state.diffFiles.find((f) => f.file_path === filePath); + [types.TOGGLE_DIFF_FILE_RENDERING_MORE](filePath) { + const file = this.diffFiles.find((f) => f.file_path === filePath); file.renderingLines = !file.renderingLines; }, - [types.SET_DIFF_FILE_VIEWER](state, { filePath, viewer }) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.SET_DIFF_FILE_VIEWER]({ filePath, viewer }) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.viewer = viewer; }, - [types.SET_SHOW_SUGGEST_POPOVER](state) { - state.showSuggestPopover = false; + [types.SET_SHOW_SUGGEST_POPOVER]() { + this.showSuggestPopover = false; }, - [types.SET_FILE_BY_FILE](state, fileByFile) { - state.viewDiffsFileByFile = fileByFile; + [types.SET_FILE_BY_FILE](fileByFile) { + this.viewDiffsFileByFile = fileByFile; }, - [types.SET_MR_FILE_REVIEWS](state, newReviews) { - state.mrReviews = newReviews; + [types.SET_MR_FILE_REVIEWS](newReviews) { + this.mrReviews = newReviews; }, - [types.DISABLE_VIRTUAL_SCROLLING](state) { - state.disableVirtualScroller = true; + [types.DISABLE_VIRTUAL_SCROLLING]() { + this.disableVirtualScroller = true; }, - [types.TOGGLE_FILE_COMMENT_FORM](state, filePath) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.TOGGLE_FILE_COMMENT_FORM](filePath) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.hasCommentForm = !file.hasCommentForm; }, - [types.SET_FILE_COMMENT_FORM](state, { filePath, expanded }) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.SET_FILE_COMMENT_FORM]({ filePath, expanded }) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file.hasCommentForm = expanded; }, - [types.ADD_DRAFT_TO_FILE](state, { filePath, draft }) { - const file = findDiffFile(state.diffFiles, filePath, 'file_path'); + [types.ADD_DRAFT_TO_FILE]({ filePath, draft }) { + const file = findDiffFile(this.diffFiles, filePath, 'file_path'); file?.drafts.push(draft); }, - [types.SET_LINKED_FILE_HASH](state, fileHash) { - state.linkedFileHash = fileHash; + [types.SET_LINKED_FILE_HASH](fileHash) { + this.linkedFileHash = fileHash; }, }; diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index 213763d7857..81b0fad47a0 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -11,10 +11,18 @@ "GroupAuditEventStreamingDestination", "InstanceAuditEventStreamingDestination" ], + "BaseDiscussionInterface": [ + "AbuseReportDiscussion", + "Discussion" + ], "BaseHeaderInterface": [ "AuditEventStreamingHeader", "AuditEventsStreamingInstanceHeader" ], + "BaseNoteInterface": [ + "AbuseReportNote", + "Note" + ], "CiRunnerCloudProvisioning": [ "CiRunnerGoogleCloudProvisioning" ], @@ -85,7 +93,6 @@ "ProjectMember" ], "NoteableInterface": [ - "AbuseReport", "AlertManagementAlert", "BoardEpic", "Design", @@ -138,6 +145,8 @@ "UploadRegistry" ], "ResolvableInterface": [ + "AbuseReportDiscussion", + "AbuseReportNote", "Discussion", "Note" ], diff --git a/app/assets/javascripts/mr_notes/store/legacy_mr_notes.js b/app/assets/javascripts/mr_notes/store/legacy_mr_notes.js new file mode 100644 index 00000000000..d59b133f4c2 --- /dev/null +++ b/app/assets/javascripts/mr_notes/store/legacy_mr_notes.js @@ -0,0 +1,3 @@ +import { defineStore } from 'pinia'; + +export const useMrNotes = defineStore('legacyMrNotes', {}); diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index 8dbce435dc9..be655ee1f6d 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_form.vue +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -89,6 +89,7 @@ export default { :value="commentLineStart" :options="commentLineOptions" width="sm" + class="gl-w-auto" @change="updateCommentLineStart" /> diff --git a/app/assets/javascripts/notes/store/legacy_notes/index.js b/app/assets/javascripts/notes/store/legacy_notes/index.js new file mode 100644 index 00000000000..5c4dd4d23e5 --- /dev/null +++ b/app/assets/javascripts/notes/store/legacy_notes/index.js @@ -0,0 +1,3 @@ +import { defineStore } from 'pinia'; + +export const useNotes = defineStore('legacyNotes', {}); diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 66355317d18..db882f78b29 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -177,18 +177,6 @@ } } - &.user-authored { - cursor: default; - background-color: $gray-10; - border-color: $gray-100; - color: $gl-text-color-disabled; - - gl-emoji { - opacity: 0.4; - filter: grayscale(100%); - } - } - &.btn { &:focus { outline: 0; @@ -196,7 +184,6 @@ } &.is-loading { - .award-control-icon-normal, .emoji-icon { display: none; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 5d35a71f3f1..c034df6b626 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -42,11 +42,6 @@ margin-bottom: 0; } - &.clear-block { - margin-bottom: $gl-padding - 1px; - padding-bottom: $gl-padding; - } - &.second-block { margin-top: -1px; margin-bottom: 0; @@ -77,10 +72,6 @@ > .controls { float: right; } - - .new-branch { - margin-top: 3px; - } } .content-block-small { diff --git a/app/assets/stylesheets/framework/breadcrumbs.scss b/app/assets/stylesheets/framework/breadcrumbs.scss index c56cd09c5d4..2ccaeb19326 100644 --- a/app/assets/stylesheets/framework/breadcrumbs.scss +++ b/app/assets/stylesheets/framework/breadcrumbs.scss @@ -32,25 +32,3 @@ flex-grow: 1; } } - -/* - * This temporarily restores the legacy breadcrumbs styles on the primary HAML breadcrumbs. - * Those styles got changed in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3663, - * causing a regression in this particular instance which does not use a Vue component and is - * therefore unable to collapse overflowing items within a disclosure dropdown. - * These temporary overrides will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/358113. - */ -.tmp-breadcrumbs-fix { - .gl-breadcrumb-list { - flex-wrap: wrap; - max-width: none; - - .gl-breadcrumb-item { - > a { - @include media-breakpoint-down(xs) { - @include str-truncated($breadcrumb-max-width); - } - } - } - } -} diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss index d5cd28718aa..df5970b0e34 100644 --- a/app/assets/stylesheets/framework/broadcast_messages.scss +++ b/app/assets/stylesheets/framework/broadcast_messages.scss @@ -41,12 +41,6 @@ } } -.toggle-colors { - input { - min-height: 34px; - } -} - .gl-broadcast-message-content p:last-child { margin: 0; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 5df5cbbe126..ee3351e9374 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -20,10 +20,6 @@ color: $text; border-color: $border; - &.btn-border-color { - border-color: $border-color; - } - > .icon { color: $text; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 68f1452bebb..e320aa7251e 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -199,12 +199,6 @@ p.time { // Fix issue with notes & lists creating a bunch of bottom borders. li.note { img { max-width: 100%; } - - .note-title { - li { - border-bottom: 0 !important; - } - } } .markdown { @@ -287,6 +281,7 @@ li.note { } } +// these classes override styles from the dropzone node package .dropzone .dz-preview .dz-progress { border-color: $border-color !important; @@ -345,16 +340,6 @@ li.note { word-wrap: break-word; } -.checkbox-icon-inline-wrapper { - .checkbox { - display: inline; - - label { - display: inline; - } - } -} - /** COMMON CLASSES **/ /** 🚨 Do not use these classes — they are deprecated and being removed. 🚨 diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index 71d16b92019..c6629180e87 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -82,11 +82,6 @@ $diff-file-header: 41px; cursor: pointer; } - .file-mode-changed { - padding: 10px; - @apply gl-text-subtle; - } - .suppressed-container { padding: ($padding-base-vertical + 5px) $padding-base-horizontal; text-align: center; @@ -455,12 +450,6 @@ table.code { span { white-space: break-spaces; - &.context-cell { - display: inline-block; - width: 100%; - height: 100%; - } - &.line { word-wrap: break-word; } @@ -820,10 +809,6 @@ table.code { &[aria-expanded="false"] { @apply gl-border-b; } - - .reply-author-avatar { - height: 1.5rem; - } } } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 128755d59e8..113f2935850 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -104,10 +104,6 @@ color: $gl-text-color !important; } - &.no-outline { - outline: 0; - } - &.large { width: 200px; } @@ -241,13 +237,6 @@ display: block; } } - - .icon-play { - fill: $gl-text-color-secondary; - margin-right: 6px; - height: 12px; - width: 11px; - } } .dropdown-menu { @@ -272,11 +261,6 @@ margin-bottom: $dropdown-vertical-offset; } - &.dropdown-open-left { - right: 0; - left: auto; - } - &.is-loading { .dropdown-content { display: none; @@ -360,12 +344,6 @@ text-transform: capitalize; } - .dropdown-bold-header { - font-weight: $gl-font-weight-bold; - line-height: $gl-line-height; - padding: $dropdown-item-padding-y $dropdown-item-padding-x; - } - .unclickable { cursor: not-allowed; padding: 5px 8px; @@ -376,10 +354,6 @@ // Expects up to 3 digits on the badge margin-right: 40px; } - - .dropdown-menu-content { - padding: $dropdown-item-padding-y $dropdown-item-padding-x; - } } .dropdown-item { @@ -462,10 +436,6 @@ } } -.dropdown-menu-large { - width: 340px; -} - .dropdown-menu-full-width { width: 100%; } @@ -537,7 +507,7 @@ li { a, button, - .dropdown-item:not(.open-with-link) { + .dropdown-item { padding: 8px 40px; position: relative; @@ -656,8 +626,7 @@ } } -.dropdown-input-field, -.default-dropdown-input { +.dropdown-input-field { background-color: $input-bg; display: block; width: 100%; @@ -808,31 +777,6 @@ } } -.dropdown-content-faded-mask { - position: relative; - - .dropdown-list { - max-height: $dropdown-max-height; - overflow-y: auto; - position: relative; - } - - &::after { - height: $dropdown-fade-mask-height; - width: 100%; - position: absolute; - bottom: 0; - background: linear-gradient(to top, $white 0, rgba($white, 0)); - transition: opacity $fade-mask-transition-duration $fade-mask-transition-curve; - content: ''; - pointer-events: none; - } - - &.fade-out::after { - opacity: 0; - } -} - .labels-select-wrapper { &.is-standalone { min-width: $input-md-width; diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss index a9acb823cf3..563a1a8c717 100644 --- a/app/assets/stylesheets/framework/vue_transitions.scss +++ b/app/assets/stylesheets/framework/vue_transitions.scss @@ -1,3 +1,8 @@ +/** +* Transition classes are built dynamically, please read about vue-transitions +* here before removing https://vuejs.org/guide/built-ins/transition +**/ + .fade-enter-active, .fade-leave-active, .fade-in-enter-active, diff --git a/app/graphql/resolvers/noteable/notes_resolver.rb b/app/graphql/resolvers/noteable/notes_resolver.rb index a8d75d8053e..b4bd1068723 100644 --- a/app/graphql/resolvers/noteable/notes_resolver.rb +++ b/app/graphql/resolvers/noteable/notes_resolver.rb @@ -22,9 +22,6 @@ module Resolvers end def resolve_with_lookahead(**args) - # TODO: Implement as part of completion https://gitlab.com/gitlab-org/gitlab/-/issues/458264 - return [] if object.is_a?(AbuseReport) - notes = NotesFinder.new(current_user, build_params(args)).execute apply_lookahead(notes) end diff --git a/app/graphql/types/abuse_report_type.rb b/app/graphql/types/abuse_report_type.rb index dc40800af94..1ba3a9916cb 100644 --- a/app/graphql/types/abuse_report_type.rb +++ b/app/graphql/types/abuse_report_type.rb @@ -4,8 +4,6 @@ module Types class AbuseReportType < BaseObject graphql_name 'AbuseReport' - implements Types::Notes::NoteableInterface - description 'An abuse report' authorize :read_abuse_report @@ -15,5 +13,10 @@ module Types field :labels, ::Types::LabelType.connection_type, null: true, description: 'Labels of the abuse report.' + + field :discussions, ::Types::Notes::AbuseReport::DiscussionType.connection_type, + null: false, description: "All discussions on the noteable." + field :notes, ::Types::Notes::AbuseReport::NoteType.connection_type, + null: false, description: "All notes on the noteable." end end diff --git a/app/graphql/types/notes/abuse_report/discussion_type.rb b/app/graphql/types/notes/abuse_report/discussion_type.rb new file mode 100644 index 00000000000..b7c8cab3e59 --- /dev/null +++ b/app/graphql/types/notes/abuse_report/discussion_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module Notes + module AbuseReport + class DiscussionType < BaseObject + graphql_name 'AbuseReportDiscussion' + + authorize :read_note + + DiscussionID = ::Types::GlobalIDType[::Discussion] + + implements Types::Notes::BaseDiscussionInterface + + field :abuse_report, Types::AbuseReportType, null: true, + description: 'Abuse report which the discussion belongs to.' + + field :notes, Types::Notes::AbuseReport::NoteType.connection_type, null: false, + description: 'All notes in the discussion.' + end + end + end +end diff --git a/app/graphql/types/notes/abuse_report/note_type.rb b/app/graphql/types/notes/abuse_report/note_type.rb new file mode 100644 index 00000000000..c31caf4c9bd --- /dev/null +++ b/app/graphql/types/notes/abuse_report/note_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Notes + module AbuseReport + class NoteType < BaseObject + graphql_name 'AbuseReportNote' + + implements Types::Notes::BaseNoteInterface + + authorize :read_note + + field :id, ::Types::GlobalIDType[::AntiAbuse::Reports::Note], + null: false, + description: 'ID of the note.' + + field :discussion, Types::Notes::AbuseReport::DiscussionType, + null: true, + description: 'Discussion the note is a part of.' + + def note_project + nil + end + end + end + end +end diff --git a/app/graphql/types/notes/base_discussion_interface.rb b/app/graphql/types/notes/base_discussion_interface.rb new file mode 100644 index 00000000000..174c74eb4f2 --- /dev/null +++ b/app/graphql/types/notes/base_discussion_interface.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Notes + module BaseDiscussionInterface + include Types::BaseInterface + + DiscussionID = ::Types::GlobalIDType[::Discussion] + + implements Types::ResolvableInterface + + field :created_at, Types::TimeType, null: false, + description: "Timestamp of the discussion's creation." + field :id, DiscussionID, null: false, + description: "ID of the discussion." + field :reply_id, DiscussionID, null: false, + description: 'ID used to reply to the discussion.' + + # DiscussionID.coerce_result is suitable here, but will always mark this + # as being a 'Discussion'. Using `GlobalId.build` guarantees that we get + # the correct class, and that it matches `id`. + def reply_id + ::Gitlab::GlobalId.build(object, id: object.reply_id) + end + end + end +end diff --git a/app/graphql/types/notes/base_note_interface.rb b/app/graphql/types/notes/base_note_interface.rb new file mode 100644 index 00000000000..ff0e2be1333 --- /dev/null +++ b/app/graphql/types/notes/base_note_interface.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Types + module Notes + module BaseNoteInterface + include Types::BaseInterface + + implements Types::ResolvableInterface + + field :author, Types::UserType, + null: true, + description: 'User who wrote the note.' + + field :award_emoji, Types::AwardEmojis::AwardEmojiType.connection_type, + null: true, + description: 'List of emoji reactions associated with the note.' + + field :body, GraphQL::Types::String, + null: false, + method: :note, + description: 'Content of the note.' + + field :body_first_line_html, GraphQL::Types::String, + null: false, + description: 'First line of the note content.' + + field :body_html, GraphQL::Types::String, + method: :note_html, + null: true, + description: "GitLab Flavored Markdown rendering of the content of the note." + + field :created_at, Types::TimeType, + null: false, + description: 'Timestamp of the note creation.' + + field :last_edited_at, Types::TimeType, + null: true, + description: 'Timestamp when note was last edited.' + + field :last_edited_by, Types::UserType, + null: true, + description: 'User who last edited the note.' + + field :updated_at, Types::TimeType, + null: false, + description: "Timestamp of the note's last activity." + + field :url, GraphQL::Types::String, + null: true, + description: 'URL to view the note in the Web UI.' + + def author + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find + end + + def url + ::Gitlab::UrlBuilder.build(object) + end + + def body_first_line_html + first_line_in_markdown(object, :note, 125, project: note_project) + end + end + end +end diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb index fa2a1a26a51..69ea3d8974c 100644 --- a/app/graphql/types/notes/discussion_type.rb +++ b/app/graphql/types/notes/discussion_type.rb @@ -5,29 +5,14 @@ module Types class DiscussionType < BaseObject graphql_name 'Discussion' - DiscussionID = ::Types::GlobalIDType[::Discussion] - authorize :read_note - implements Types::ResolvableInterface + implements Types::Notes::BaseDiscussionInterface - field :created_at, Types::TimeType, null: false, - description: "Timestamp of the discussion's creation." - field :id, DiscussionID, null: false, - description: "ID of this discussion." field :noteable, Types::NoteableType, null: true, description: 'Object which the discussion belongs to.' field :notes, Types::Notes::NoteType.connection_type, null: false, description: 'All notes in the discussion.' - field :reply_id, DiscussionID, null: false, - description: 'ID used to reply to this discussion.' - - # DiscussionID.coerce_result is suitable here, but will always mark this - # as being a 'Discussion'. Using `GlobalId.build` guarantees that we get - # the correct class, and that it matches `id`. - def reply_id - ::Gitlab::GlobalId.build(object, id: object.reply_id) - end def noteable noteable = object.noteable diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index 791b76de30d..7808a45c97e 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -14,7 +14,7 @@ module Types expose_permissions Types::PermissionTypes::Note - implements Types::ResolvableInterface + implements Types::Notes::BaseNoteInterface present_using NotePresenter @@ -31,10 +31,6 @@ module Types null: true, description: 'Project associated with the note.' - field :author, Types::UserType, - null: true, - description: 'User who wrote the note.' - field :system, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the note was created by the system or by a user.' @@ -43,19 +39,6 @@ module Types null: true, description: 'Name of the icon corresponding to a system note.' - field :body, GraphQL::Types::String, - null: false, - method: :note, - description: 'Content of the note.' - - field :body_first_line_html, GraphQL::Types::String, - null: false, - description: 'First line of the note content.' - - field :award_emoji, Types::AwardEmojis::AwardEmojiType.connection_type, - null: true, - description: 'List of emoji reactions associated with the note.' - field :imported, GraphQL::Types::Boolean, null: true, description: 'Indicates whether the note was imported.', @@ -65,28 +48,12 @@ module Types description: 'Indicates if the note is internal.', method: :confidential? - field :created_at, Types::TimeType, - null: false, - description: 'Timestamp of the note creation.' field :discussion, Types::Notes::DiscussionType, null: true, description: 'Discussion the note is a part of.' field :position, Types::Notes::DiffPositionType, null: true, description: 'Position of the note on a diff.' - field :updated_at, Types::TimeType, - null: false, - description: "Timestamp of the note's last activity." - field :url, GraphQL::Types::String, - null: true, - description: 'URL to view the note in the Web UI.' - - field :last_edited_at, Types::TimeType, - null: true, - description: 'Timestamp when note was last edited.' - field :last_edited_by, Types::UserType, - null: true, - description: 'User who last edited the note.' field :author_is_contributor, GraphQL::Types::Boolean, null: true, @@ -98,15 +65,6 @@ module Types null: true, description: 'Metadata for the given note if it is a system note.' - field :body_html, GraphQL::Types::String, - method: :note_html, - null: true, - description: "GitLab Flavored Markdown rendering of the content of the note." - - def url - ::Gitlab::UrlBuilder.build(object) - end - def system_note_icon_name SystemNoteHelper.system_note_icon_name(object) if object.system? end @@ -115,10 +73,6 @@ module Types Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find end - def author - Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find - end - # We now support also SyntheticNote notes as a NoteType, but SyntheticNote does not have a real note ID, # as SyntheticNote is generated dynamically from a ResourceEvent instance. def id @@ -128,8 +82,8 @@ module Types ::Gitlab::GlobalId.build(object, model_name: object.object.class.to_s, id: object.discussion_id) end - def body_first_line_html - first_line_in_markdown(object, :note, 125, project: object.project) + def note_project + object.project end end end diff --git a/app/graphql/types/notes/noteable_interface.rb b/app/graphql/types/notes/noteable_interface.rb index cfe091ed255..9971511d6ce 100644 --- a/app/graphql/types/notes/noteable_interface.rb +++ b/app/graphql/types/notes/noteable_interface.rb @@ -21,17 +21,12 @@ module Types Types::DesignManagement::DesignType when ::AlertManagement::Alert Types::AlertManagement::AlertType - when AbuseReport - Types::AbuseReportType else raise "Unknown GraphQL type for #{object}" end end def commenters - # TODO: Implement as part of completion https://gitlab.com/gitlab-org/gitlab/-/issues/458264 - return [] if object.is_a?(AbuseReport) - object.commenters(user: current_user) end end diff --git a/app/models/anti_abuse/reports/discussion.rb b/app/models/anti_abuse/reports/discussion.rb index b169b619828..b4246ba7b44 100644 --- a/app/models/anti_abuse/reports/discussion.rb +++ b/app/models/anti_abuse/reports/discussion.rb @@ -3,6 +3,8 @@ module AntiAbuse module Reports class Discussion < ::Discussion + delegate :abuse_report, to: :first_note + def self.base_discussion_id(_note) [:discussion, :abuse_report_id] end diff --git a/app/models/anti_abuse/reports/note.rb b/app/models/anti_abuse/reports/note.rb index 19be2f9a65b..e64bcee3aff 100644 --- a/app/models/anti_abuse/reports/note.rb +++ b/app/models/anti_abuse/reports/note.rb @@ -32,6 +32,19 @@ module AntiAbuse validates :abuse_report, presence: true scope :fresh, -> { order_created_asc.with_order_id_asc } + scope :inc_relations_for_view, ->(_abuse_report = nil) do + relations = [ + { author: :status }, :updated_by, :award_emoji + ] + + includes(relations) + end + + class << self + def parent_object_field + :abuse_report + end + end def discussion_class(_noteable = nil) AntiAbuse::Reports::IndividualNoteDiscussion diff --git a/app/models/concerns/notes/discussion.rb b/app/models/concerns/notes/discussion.rb index 57c8d7c6343..658861beaea 100644 --- a/app/models/concerns/notes/discussion.rb +++ b/app/models/concerns/notes/discussion.rb @@ -11,7 +11,7 @@ module Notes class_methods do def discussions(context_noteable = nil) - ::Discussion.build_collection(all.includes(:noteable).fresh, context_noteable) + ::Discussion.build_collection(all.includes(parent_object_field).fresh, context_noteable) end def find_discussion(discussion_id) diff --git a/app/models/integrations/instance_integration.rb b/app/models/integrations/instance_integration.rb new file mode 100644 index 00000000000..9fc52fb5708 --- /dev/null +++ b/app/models/integrations/instance_integration.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Integrations + class InstanceIntegration < Integration + self.table_name = 'instance_integrations' + end +end diff --git a/app/models/note.rb b/app/models/note.rb index d0cb30529a1..a0936cee8f8 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -230,6 +230,10 @@ class Note < ApplicationRecord ActiveModel::Name.new(self, nil, 'note') end + def parent_object_field + :noteable + end + # Group diff discussions by line code or file path. # It is not needed to group by line code when comment is # on an image. diff --git a/app/policies/anti_abuse/reports/note_policy.rb b/app/policies/anti_abuse/reports/note_policy.rb new file mode 100644 index 00000000000..522fec21800 --- /dev/null +++ b/app/policies/anti_abuse/reports/note_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AntiAbuse + module Reports + class NotePolicy < BasePolicy + delegate { @subject.abuse_report } + end + end +end diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index b0cff84e50c..2ea4bbb1102 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -4,7 +4,7 @@ expanded: expanded) do |c| - c.with_description do = s_('ExternalAuthorization|External classification policy authorization.') - = link_to _('Learn more.'), help_page_path('administration/settings/external_authorization'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('administration/settings/external_authorization.md'), target: '_blank', rel: 'noopener noreferrer' - c.with_body do = gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form', id: 'external-auth-settings' } do |f| = form_errors(@application_setting) diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml index 3171f019abb..d19a90a8c2b 100644 --- a/app/views/admin/impersonation_tokens/index.html.haml +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -28,7 +28,7 @@ impersonation: true, token: @impersonation_token, scopes: @scopes, - help_path: help_page_path('api/rest/index', anchor: 'impersonation-tokens') + help_path: help_page_path('api/rest/index.md', anchor: 'impersonation-tokens') - c.with_body do #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json } } diff --git a/app/views/groups/settings/_resource_access_token_creation.html.haml b/app/views/groups/settings/_resource_access_token_creation.html.haml index 7d64ab84ad2..e3a04678daa 100644 --- a/app/views/groups/settings/_resource_access_token_creation.html.haml +++ b/app/views/groups/settings/_resource_access_token_creation.html.haml @@ -1,8 +1,8 @@ - return unless render_setting_to_allow_project_access_token_creation?(group) .form-group.gl-mb-3 - - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens') - - group_access_tokens_link = help_page_path('user/group/settings/group_access_tokens') + - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens.md') + - group_access_tokens_link = help_page_path('user/group/settings/group_access_tokens.md') - link_start_project = ''.html_safe % { url: project_access_tokens_link } - link_start_group = ''.html_safe % { url: group_access_tokens_link } = f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed, diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml index cf44f2b69b1..e7bd0392105 100644 --- a/app/views/groups/settings/_two_factor_auth.html.haml +++ b/app/views/groups/settings/_two_factor_auth.html.haml @@ -1,5 +1,5 @@ - return unless group.parent_allows_two_factor_authentication? -- docs_link_url = help_page_path('security/two_factor_authentication', anchor: 'enforce-2fa-for-all-users-in-a-group') +- docs_link_url = help_page_path('security/two_factor_authentication.md', anchor: 'enforce-2fa-for-all-users-in-a-group') - docs_link_start = ''.html_safe % { url: docs_link_url } %h5= _('Two-factor authentication') diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml index 2b31feaa6d4..41d66e9f6a7 100644 --- a/app/views/groups/settings/access_tokens/index.html.haml +++ b/app/views/groups/settings/access_tokens/index.html.haml @@ -6,7 +6,7 @@ = render ::Layouts::SettingsSectionComponent.new(page_title, options: { class: 'js-search-settings-section' }) do |c| - c.with_description do - - help_link_start = ''.html_safe % { url: help_page_path('user/group/settings/group_access_tokens') } + - help_link_start = ''.html_safe % { url: help_page_path('user/group/settings/group_access_tokens.md') } - if current_user.can?(:create_resource_access_tokens, @group) = _('Generate group access tokens scoped to this group for your applications that need access to the GitLab API.') = html_escape(_('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}')) % { link_start: help_link_start, link_end: ''.html_safe } @@ -46,7 +46,7 @@ default_access_level: Gitlab::Access::GUEST, prefix: :resource_access_token, description_prefix: :group_access_token, - help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token') + help_path: help_page_path('user/group/settings/group_access_tokens.md', anchor: 'scopes-for-a-group-access-token') - c.with_body do #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true } } diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 89b35c847e4..d1de043b21d 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -17,7 +17,7 @@ %p - register_2fa_token = _('We recommend using cloud-based authenticator applications that can restore access if you lose your hardware device.') = register_2fa_token.html_safe - = link_to _('What are some examples?'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'enable-one-time-password'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('What are some examples?'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'enable-one-time-password'), target: '_blank', rel: 'noopener noreferrer' .gl-p-2.gl-mb-3{ style: 'background: #fff' } = raw @qr_code .gl-mb-5 @@ -41,7 +41,7 @@ alert_options: { class: 'gl-mb-3' }, dismissible: false) do |c| - c.with_body do - = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication_troubleshooting'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication_troubleshooting.md'), target: '_blank', rel: 'noopener noreferrer' - if current_password_required? .form-group @@ -130,7 +130,7 @@ alert_options: { class: 'gl-mb-3' }, dismissible: false) do |c| - c.with_body do - = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication_troubleshooting'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication_troubleshooting.md'), target: '_blank', rel: 'noopener noreferrer' .js-manage-two-factor-form{ data: { current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } } - else %p diff --git a/app/views/projects/settings/access_tokens/_form.html.haml b/app/views/projects/settings/access_tokens/_form.html.haml index ee993962c7a..298b8359890 100644 --- a/app/views/projects/settings/access_tokens/_form.html.haml +++ b/app/views/projects/settings/access_tokens/_form.html.haml @@ -11,4 +11,4 @@ default_access_level: Gitlab::Access::GUEST, prefix: :resource_access_token, description_prefix: :project_access_token, - help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token') + help_path: help_page_path('user/project/settings/project_access_tokens.md', anchor: 'scopes-for-a-project-access-token') diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml index 4249fec3717..3350b6e89f2 100644 --- a/app/views/projects/settings/access_tokens/index.html.haml +++ b/app/views/projects/settings/access_tokens/index.html.haml @@ -10,7 +10,7 @@ %h4.gl-my-0 = page_title %p.gl-text-secondary - - help_link_start = ''.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') } + - help_link_start = ''.html_safe % { url: help_page_path('user/project/settings/project_access_tokens.md') } - if current_user.can?(:create_resource_access_tokens, @project) = _('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.') = _('You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: help_link_start, link_end: ''.html_safe } diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml index e372dbd983c..ce2a6fc09da 100644 --- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml +++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml @@ -6,7 +6,7 @@ close_button_options: { data: { testid: 'close-account-recovery-regular-check-callout' }}) do |c| - c.with_body do = s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.') - = link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer' - c.with_actions do = link_button_to profile_two_factor_auth_path, class: 'deferred-link gl-alert-action', variant: :confirm do = s_('Profiles|Manage two-factor authentication') diff --git a/app/views/user_settings/personal_access_tokens/index.html.haml b/app/views/user_settings/personal_access_tokens/index.html.haml index e092af3215e..1a912f2aeec 100644 --- a/app/views/user_settings/personal_access_tokens/index.html.haml +++ b/app/views/user_settings/personal_access_tokens/index.html.haml @@ -31,7 +31,7 @@ path: user_settings_personal_access_tokens_path, token: @personal_access_token, scopes: @scopes, - help_path: help_page_path('user/profile/personal_access_tokens', anchor: 'personal-access-token-scopes') + help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes') - c.with_body do #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } } diff --git a/config/audit_events/types/self_hosted_model_destroyed.yml b/config/audit_events/types/self_hosted_model_destroyed.yml new file mode 100644 index 00000000000..970227e2347 --- /dev/null +++ b/config/audit_events/types/self_hosted_model_destroyed.yml @@ -0,0 +1,9 @@ +name: self_hosted_model_destroyed +description: A new self-hosted model configuration was destroyed +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/477999 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165321 +feature_category: self-hosted_models +milestone: '17.4' +saved_to_database: true +scope: [Instance, User] +streamed: true diff --git a/config/initializers/gitlab_experiment.rb b/config/initializers/gitlab_experiment.rb index 13554fc0bfb..8494f49bde6 100644 --- a/config/initializers/gitlab_experiment.rb +++ b/config/initializers/gitlab_experiment.rb @@ -13,7 +13,7 @@ Gitlab::Experiment.configure do |config| # Customize the logic of our default rollout, which shouldn't include # assigning the control yet -- we specifically set it to false for now. # - config.default_rollout = Gitlab::Experiment::Rollout.resolve(:feature) + config.default_rollout = Gitlab::Experiment::Rollout.resolve('Gitlab::ExperimentFeatureRollout') # Mount the engine and middleware at a gitlab friendly style path. # diff --git a/db/docs/instance_integrations.yml b/db/docs/instance_integrations.yml new file mode 100644 index 00000000000..b799f7a5a5b --- /dev/null +++ b/db/docs/instance_integrations.yml @@ -0,0 +1,10 @@ +--- +table_name: instance_integrations +classes: +- Integrations::InstanceIntegration +feature_categories: +- integrations +description: Support 3rd party instance-wide integrations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164503 +milestone: '17.4' +gitlab_schema: gitlab_main_clusterwide diff --git a/db/migrate/20240828103148_add_spp_repository_pipeline_access_to_project_settings.rb b/db/migrate/20240828103148_add_spp_repository_pipeline_access_to_project_settings.rb new file mode 100644 index 00000000000..2d1c274ab38 --- /dev/null +++ b/db/migrate/20240828103148_add_spp_repository_pipeline_access_to_project_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddSppRepositoryPipelineAccessToProjectSettings < Gitlab::Database::Migration[2.2] + enable_lock_retries! + milestone '17.4' + + def change + add_column :project_settings, :spp_repository_pipeline_access, :boolean + end +end diff --git a/db/migrate/20240829163210_create_instance_integrations_table.rb b/db/migrate/20240829163210_create_instance_integrations_table.rb new file mode 100644 index 00000000000..85128e8dc86 --- /dev/null +++ b/db/migrate/20240829163210_create_instance_integrations_table.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class CreateInstanceIntegrationsTable < Gitlab::Database::Migration[2.2] + milestone '17.4' + + # rubocop:disable Migration/EnsureFactoryForTable -- False Positive + def up + create_table :instance_integrations, id: :bigserial do |t| + t.timestamps_with_timezone null: false + t.integer :comment_detail + t.boolean :active, default: false, null: false + t.boolean :push_events, default: true + t.boolean :issues_events, default: true + t.boolean :merge_requests_events, default: true + t.boolean :tag_push_events, default: true + t.boolean :note_events, default: true, null: false + t.boolean :wiki_page_events, default: true + t.boolean :pipeline_events, default: false, null: false + t.boolean :confidential_issues_events, default: true, null: false + t.boolean :commit_events, default: true, null: false + t.boolean :job_events, default: false, null: false + t.boolean :confidential_note_events, default: true + t.boolean :deployment_events, default: false, null: false + t.boolean :comment_on_event_enabled, default: true, null: false + t.boolean :alert_events + t.boolean :vulnerability_events, default: false, null: false + t.boolean :archive_trace_events, default: false, null: false + t.boolean :incident_events, default: false, null: false + t.boolean :group_mention_events, default: false, null: false + t.boolean :group_confidential_mention_events, default: false, null: false + t.text :category, default: 'common', limit: 255 + t.text :type, limit: 255 + t.binary :encrypted_properties + t.binary :encrypted_properties_iv + end + end + # rubocop:enable Migration/EnsureFactoryForTable -- False Positive + + def down + drop_table :instance_integrations, if_exists: true + end +end diff --git a/db/schema_migrations/20240828103148 b/db/schema_migrations/20240828103148 new file mode 100644 index 00000000000..d5a5dcbeb52 --- /dev/null +++ b/db/schema_migrations/20240828103148 @@ -0,0 +1 @@ +3a6da002969d32ed71e3a8700a007380d20193df5f3a3591e98136a135380f4f \ No newline at end of file diff --git a/db/schema_migrations/20240829163210 b/db/schema_migrations/20240829163210 new file mode 100644 index 00000000000..ebc3b67fc32 --- /dev/null +++ b/db/schema_migrations/20240829163210 @@ -0,0 +1 @@ +7f15b5117aa826e8e0dbbd334393a29c4da7bc6443308f1ceffbaa015ab999cd \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 91b21383f49..bc19fdb661f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12059,6 +12059,48 @@ CREATE SEQUENCE instance_audit_events_streaming_headers_id_seq ALTER SEQUENCE instance_audit_events_streaming_headers_id_seq OWNED BY instance_audit_events_streaming_headers.id; +CREATE TABLE instance_integrations ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + comment_detail integer, + active boolean DEFAULT false NOT NULL, + push_events boolean DEFAULT true, + issues_events boolean DEFAULT true, + merge_requests_events boolean DEFAULT true, + tag_push_events boolean DEFAULT true, + note_events boolean DEFAULT true NOT NULL, + wiki_page_events boolean DEFAULT true, + pipeline_events boolean DEFAULT false NOT NULL, + confidential_issues_events boolean DEFAULT true NOT NULL, + commit_events boolean DEFAULT true NOT NULL, + job_events boolean DEFAULT false NOT NULL, + confidential_note_events boolean DEFAULT true, + deployment_events boolean DEFAULT false NOT NULL, + comment_on_event_enabled boolean DEFAULT true NOT NULL, + alert_events boolean, + vulnerability_events boolean DEFAULT false NOT NULL, + archive_trace_events boolean DEFAULT false NOT NULL, + incident_events boolean DEFAULT false NOT NULL, + group_mention_events boolean DEFAULT false NOT NULL, + group_confidential_mention_events boolean DEFAULT false NOT NULL, + category text DEFAULT 'common'::text, + type text, + encrypted_properties bytea, + encrypted_properties_iv bytea, + CONSTRAINT check_611836812c CHECK ((char_length(category) <= 255)), + CONSTRAINT check_69b7b09aa8 CHECK ((char_length(type) <= 255)) +); + +CREATE SEQUENCE instance_integrations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE instance_integrations_id_seq OWNED BY instance_integrations.id; + CREATE TABLE integrations ( id bigint NOT NULL, project_id bigint, @@ -16719,6 +16761,7 @@ CREATE TABLE project_settings ( duo_features_enabled boolean DEFAULT true NOT NULL, require_reauthentication_to_approve boolean, observability_alerts_enabled boolean DEFAULT true NOT NULL, + spp_repository_pipeline_access boolean, CONSTRAINT check_1a30456322 CHECK ((char_length(pages_unique_domain) <= 63)), CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)), CONSTRAINT check_3ca5cbffe6 CHECK ((char_length(issue_branch_template) <= 255)), @@ -21709,6 +21752,8 @@ ALTER TABLE ONLY insights ALTER COLUMN id SET DEFAULT nextval('insights_id_seq': ALTER TABLE ONLY instance_audit_events_streaming_headers ALTER COLUMN id SET DEFAULT nextval('instance_audit_events_streaming_headers_id_seq'::regclass); +ALTER TABLE ONLY instance_integrations ALTER COLUMN id SET DEFAULT nextval('instance_integrations_id_seq'::regclass); + ALTER TABLE ONLY integrations ALTER COLUMN id SET DEFAULT nextval('integrations_id_seq'::regclass); ALTER TABLE ONLY internal_ids ALTER COLUMN id SET DEFAULT nextval('internal_ids_id_seq'::regclass); @@ -23973,6 +24018,9 @@ ALTER TABLE ONLY instance_audit_events ALTER TABLE ONLY instance_audit_events_streaming_headers ADD CONSTRAINT instance_audit_events_streaming_headers_pkey PRIMARY KEY (id); +ALTER TABLE ONLY instance_integrations + ADD CONSTRAINT instance_integrations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY integrations ADD CONSTRAINT integrations_pkey PRIMARY KEY (id); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5b0ff90be95..f921c83cb21 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10880,6 +10880,52 @@ Some of the types in the schema exist solely to model connections. Each connecti has a distinct, named type, with a distinct named edge type. These are listed separately below. +#### `AbuseReportDiscussionConnection` + +The connection type for [`AbuseReportDiscussion`](#abusereportdiscussion). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[AbuseReportDiscussionEdge]`](#abusereportdiscussionedge) | A list of edges. | +| `nodes` | [`[AbuseReportDiscussion]`](#abusereportdiscussion) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `AbuseReportDiscussionEdge` + +The edge type for [`AbuseReportDiscussion`](#abusereportdiscussion). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`AbuseReportDiscussion`](#abusereportdiscussion) | The item at the end of the edge. | + +#### `AbuseReportNoteConnection` + +The connection type for [`AbuseReportNote`](#abusereportnote). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[AbuseReportNoteEdge]`](#abusereportnoteedge) | A list of edges. | +| `nodes` | [`[AbuseReportNote]`](#abusereportnote) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `AbuseReportNoteEdge` + +The edge type for [`AbuseReportNote`](#abusereportnote). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`AbuseReportNote`](#abusereportnote) | The item at the end of the edge. | + #### `AccessLevelDeployKeyConnection` The connection type for [`AccessLevelDeployKey`](#accessleveldeploykey). @@ -16813,28 +16859,49 @@ An abuse report. | Name | Type | Description | | ---- | ---- | ----------- | -| `commenters` | [`UserCoreConnection!`](#usercoreconnection) | All commenters on this noteable. (see [Connections](#connections)) | -| `discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) | +| `discussions` | [`AbuseReportDiscussionConnection!`](#abusereportdiscussionconnection) | All discussions on the noteable. (see [Connections](#connections)) | | `id` | [`AbuseReportID!`](#abusereportid) | Global ID of the abuse report. | | `labels` | [`LabelConnection`](#labelconnection) | Labels of the abuse report. (see [Connections](#connections)) | +| `notes` | [`AbuseReportNoteConnection!`](#abusereportnoteconnection) | All notes on the noteable. (see [Connections](#connections)) | -#### Fields with arguments +### `AbuseReportDiscussion` -##### `AbuseReport.notes` - -All notes on this noteable. - -Returns [`NoteConnection!`](#noteconnection). - -This field returns a [connection](#connections). It accepts the -four standard [pagination arguments](#pagination-arguments): -`before: String`, `after: String`, `first: Int`, and `last: Int`. - -###### Arguments +#### Fields | Name | Type | Description | | ---- | ---- | ----------- | -| `filter` | [`NotesFilterType`](#notesfiltertype) | Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY. | +| `abuseReport` | [`AbuseReport`](#abusereport) | Abuse report which the discussion belongs to. | +| `createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. | +| `id` | [`DiscussionID!`](#discussionid) | ID of the discussion. | +| `notes` | [`AbuseReportNoteConnection!`](#abusereportnoteconnection) | All notes in the discussion. (see [Connections](#connections)) | +| `replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. | +| `resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. | +| `resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. | +| `resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. | +| `resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. | + +### `AbuseReportNote` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `author` | [`UserCore`](#usercore) | User who wrote the note. | +| `awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of emoji reactions associated with the note. (see [Connections](#connections)) | +| `body` | [`String!`](#string) | Content of the note. | +| `bodyFirstLineHtml` | [`String!`](#string) | First line of the note content. | +| `bodyHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of the content of the note. | +| `createdAt` | [`Time!`](#time) | Timestamp of the note creation. | +| `discussion` | [`AbuseReportDiscussion`](#abusereportdiscussion) | Discussion the note is a part of. | +| `id` | [`AntiAbuseReportsNoteID!`](#antiabusereportsnoteid) | ID of the note. | +| `lastEditedAt` | [`Time`](#time) | Timestamp when note was last edited. | +| `lastEditedBy` | [`UserCore`](#usercore) | User who last edited the note. | +| `resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. | +| `resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. | +| `resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. | +| `resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. | +| `updatedAt` | [`Time!`](#time) | Timestamp of the note's last activity. | +| `url` | [`String`](#string) | URL to view the note in the Web UI. | ### `AccessLevel` @@ -17355,6 +17422,19 @@ Information about a connected Agent. | `estimatedNextUpdateAt` | [`Time`](#time) | Estimated time when the next incremental update will happen. | | `lastUpdateAt` | [`Time`](#time) | Last incremental update time. | +### `AiAdditionalContext` + +Additional context for AI message. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `category` | [`AiAdditionalContextCategory!`](#aiadditionalcontextcategory) | Category of the additional context. | +| `content` | [`String!`](#string) | Content of the additional context. | +| `id` | [`ID!`](#id) | ID of the additional context. | +| `metadata` | [`JSON`](#json) | Metadata of the additional context. | + ### `AiAgent` An AI agent. @@ -17403,6 +17483,7 @@ AI features communication message. | Name | Type | Description | | ---- | ---- | ----------- | +| `additionalContext` | [`[AiAdditionalContext!]`](#aiadditionalcontext) | Additional context for the message. | | `agentVersionId` | [`AiAgentVersionID`](#aiagentversionid) | Global ID of the agent version to answer the message. | | `chunkId` | [`Int`](#int) | Incremental ID for a chunk from a streamed message. Null when it is not a streamed message. | | `content` | [`String`](#string) | Raw response content. | @@ -21570,10 +21651,10 @@ Aggregated summary of changes. | Name | Type | Description | | ---- | ---- | ----------- | | `createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. | -| `id` | [`DiscussionID!`](#discussionid) | ID of this discussion. | +| `id` | [`DiscussionID!`](#discussionid) | ID of the discussion. | | `noteable` | [`NoteableType`](#noteabletype) | Object which the discussion belongs to. | | `notes` | [`NoteConnection!`](#noteconnection) | All notes in the discussion. (see [Connections](#connections)) | -| `replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to this discussion. | +| `replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. | | `resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. | | `resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. | | `resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. | @@ -35427,8 +35508,11 @@ LLMs supported by the self-hosted model features. | `CODESTRAL` | Codestral 22B: Suitable for code completion and code generation. | | `DEEPSEEKCODER` | Deepseek Coder 1.3b, 6.7b and 33b base or instruct. | | `MISTRAL` | Mistral 7B: Suitable for code generation and duo chat. | +| `MISTRAL_TEXT` | Mistral-7B Text: Suitable for code completion. | | `MIXTRAL` | Mixtral 8x7B: Suitable for code generation and duo chat. | | `MIXTRAL_8X22B` | Mixtral 8x22B: Suitable for code generation and duo chat. | +| `MIXTRAL_8X22B_TEXT` | Mixtral-8x22B Text: Suitable for code completion. | +| `MIXTRAL_TEXT` | Mixtral-8x7B Text: Suitable for code completion. | ### `AiAction` @@ -38817,6 +38901,12 @@ A `AnalyticsDevopsAdoptionEnabledNamespaceID` is a global ID. It is encoded as a An example `AnalyticsDevopsAdoptionEnabledNamespaceID` is: `"gid://gitlab/Analytics::DevopsAdoption::EnabledNamespace/1"`. +### `AntiAbuseReportsNoteID` + +A `AntiAbuseReportsNoteID` is a global ID. It is encoded as a string. + +An example `AntiAbuseReportsNoteID` is: `"gid://gitlab/AntiAbuse::Reports::Note/1"`. + ### `AppSecFuzzingCoverageCorpusID` A `AppSecFuzzingCoverageCorpusID` is a global ID. It is encoded as a string. @@ -40001,6 +40091,25 @@ Implementations: | `id` | [`ID!`](#id) | ID of the destination. | | `name` | [`String!`](#string) | Name of the external destination to send audit events to. | +#### `BaseDiscussionInterface` + +Implementations: + +- [`AbuseReportDiscussion`](#abusereportdiscussion) +- [`Discussion`](#discussion) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. | +| `id` | [`DiscussionID!`](#discussionid) | ID of the discussion. | +| `replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. | +| `resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. | +| `resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. | +| `resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. | +| `resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. | + #### `BaseHeaderInterface` Implementations: @@ -40017,6 +40126,32 @@ Implementations: | `key` | [`String!`](#string) | Key of the header. | | `value` | [`String!`](#string) | Value of the header. | +#### `BaseNoteInterface` + +Implementations: + +- [`AbuseReportNote`](#abusereportnote) +- [`Note`](#note) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `author` | [`UserCore`](#usercore) | User who wrote the note. | +| `awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of emoji reactions associated with the note. (see [Connections](#connections)) | +| `body` | [`String!`](#string) | Content of the note. | +| `bodyFirstLineHtml` | [`String!`](#string) | First line of the note content. | +| `bodyHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of the content of the note. | +| `createdAt` | [`Time!`](#time) | Timestamp of the note creation. | +| `lastEditedAt` | [`Time`](#time) | Timestamp when note was last edited. | +| `lastEditedBy` | [`UserCore`](#usercore) | User who last edited the note. | +| `resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. | +| `resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. | +| `resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. | +| `resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. | +| `updatedAt` | [`Time!`](#time) | Timestamp of the note's last activity. | +| `url` | [`String`](#string) | URL to view the note in the Web UI. | + #### `CiVariable` Implementations: @@ -40211,7 +40346,6 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction). Implementations: -- [`AbuseReport`](#abusereport) - [`AlertManagementAlert`](#alertmanagementalert) - [`BoardEpic`](#boardepic) - [`Design`](#design) @@ -40288,6 +40422,8 @@ Implementations: Implementations: +- [`AbuseReportDiscussion`](#abusereportdiscussion) +- [`AbuseReportNote`](#abusereportnote) - [`Discussion`](#discussion) - [`Note`](#note) diff --git a/doc/ci/pipelines/pipeline_types.md b/doc/ci/pipelines/pipeline_types.md index 566a4528509..c45724950b6 100644 --- a/doc/ci/pipelines/pipeline_types.md +++ b/doc/ci/pipelines/pipeline_types.md @@ -10,9 +10,10 @@ DETAILS: **Tier:** Free, Premium, Ultimate **Offering:** GitLab.com, Self-managed, GitLab Dedicated -Four types of pipelines exist: +Multiple types of pipelines can run in a project, including: - Branch pipelines +- Tag pipelines - Merge request pipelines - Merge result pipelines - Merge trains @@ -32,7 +33,24 @@ Branch pipelines: - Run when you push a new commit to a branch. - Have access to [some predefined variables](../variables/predefined_variables.md). - Have access to [protected variables](../variables/index.md#protect-a-cicd-variable) - and [protected runners](../runners/configure_runners.md#prevent-runners-from-revealing-sensitive-information). + and [protected runners](../runners/configure_runners.md#prevent-runners-from-revealing-sensitive-information) + when the branch is a [protected branch](../../user/project/protected_branches.md). + +## Tag pipeline + +A pipeline can run every time you create or push a new [tag](../../user/project/repository/tags/index.md). + +This type of pipeline is called a *tag pipeline*. + +This pipeline runs by default. No configuration is required. + +Tag pipelines: + +- Run when you create/push a new tag to your repository. +- Have access to [some predefined variables](../variables/predefined_variables.md). +- Have access to [protected variables](../variables/index.md#protect-a-cicd-variable) + and [protected runners](../runners/configure_runners.md#prevent-runners-from-revealing-sensitive-information) + when the tag is a [protected tag](../../user/project/protected_tags.md). ## Merge request pipeline diff --git a/doc/ci/secrets/akeyless.md b/doc/ci/secrets/akeyless.md index 922c95cb568..a3a541d5235 100644 --- a/doc/ci/secrets/akeyless.md +++ b/doc/ci/secrets/akeyless.md @@ -22,6 +22,7 @@ Prerequisites: - Save your Akeyless access ID as a [CI/CD variable in your GitLab project](../variables/index.md#for-a-project) named `AKEYLESS_ACCESS_ID`. +- This integration only supports [static secrets](https://docs.akeyless.io/docs/static-secrets). To retrieve secrets from Akeyless, review the CI/CD configuration example that matches your use case. The `akeyless:name` keyword can contain any secrets type. diff --git a/doc/update/versions/gitlab_17_changes.md b/doc/update/versions/gitlab_17_changes.md index fc9fcbf9c73..9f87f4e1e32 100644 --- a/doc/update/versions/gitlab_17_changes.md +++ b/doc/update/versions/gitlab_17_changes.md @@ -117,17 +117,6 @@ For more information, see the: - [Deprecations and removals documentation](../../update/deprecations.md#non-expiring-access-tokens). - [Deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/369122). -## 17.4.0 - -- Starting with GitLab 17.4, new GitLab installations have a different database schema regarding ID columns. - - All previous integer (32 bits) ID columns (for example columns like `id`, `%_id`, `%_ids`) are now created as `bigint` (64 bits). - - Existing installations will migrate from 32 bit to 64 bit integers in later releases when database migrations ship to perform this change. - - If you are building a new GitLab environment to test upgrades, install GitLab 17.3 or earlier to get - the same integer types as your existing environments. You can then upgrade to later releases to run the same - database migrations as your existing environments. This isn't necessary if you're restoring from backup into the - new environment as the database restore removes the existing database schema definition and uses the definition - that's stored as part of the backup. - ## Issues to be aware of when upgrading from 17.1 and earlier - If the customer is using GitLab Duo and upgrading to GitLab 17.2.3 or earlier, they must do both of the following: @@ -211,6 +200,17 @@ bits with a `certificate key too weak` error message. Check the [GitLab documentation on securing your installation](../../security/index.md). for more details. +## 17.4.0 + +- Starting with GitLab 17.4, new GitLab installations have a different database schema regarding ID columns. + - All previous integer (32 bits) ID columns (for example columns like `id`, `%_id`, `%_ids`) are now created as `bigint` (64 bits). + - Existing installations will migrate from 32 bit to 64 bit integers in later releases when database migrations ship to perform this change. + - If you are building a new GitLab environment to test upgrades, install GitLab 17.3 or earlier to get + the same integer types as your existing environments. You can then upgrade to later releases to run the same + database migrations as your existing environments. This isn't necessary if you're restoring from backup into the + new environment as the database restore removes the existing database schema definition and uses the definition + that's stored as part of the backup. + ## 17.3.0 - Git 2.45.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../../install/installation.md#git). diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 2975ebb088a..7bb4011e38c 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -453,6 +453,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | Scope | |:------------|:------------|:------------------|:---------|:--------------|:--------------| +| [`self_hosted_model_destroyed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165321) | A new self-hosted model configuration was destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/477999) | Instance, User | | [`self_hosted_model_feature_changed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165489) | A self-hosted model feature had its configuration changed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/463215) | Project | | [`self_hosted_model_terms_accepted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165480) | Terms for usage of self-hosted models were accepted | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/477999) | Instance, User | diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index f78e125c1c3..1a820f281f1 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -264,7 +264,7 @@ After you set up your identity provider to work with GitLab, you must configure - In GitLab 17.4 and later, **Disable password authentication for enterprise users**. For more information, see the [Disable password authentication for enterprise users documentation](#disable-password-authentication-for-enterprise-users). - **Enforce SSO-only authentication for web activity for this group**. - - **Enforce SSO-only authentication for Git activity for this group**. + - **Enforce SSO-only authentication for Git and Dependency Proxy activity for this group**. For more information, see the [SSO enforcement documentation](#sso-enforcement). 1. Select **Save changes**. diff --git a/haml_lint/linter/documentation_links.rb b/haml_lint/linter/documentation_links.rb index 9fff207a81c..5c362c47abf 100644 --- a/haml_lint/linter/documentation_links.rb +++ b/haml_lint/linter/documentation_links.rb @@ -60,7 +60,7 @@ module HamlLint record_lint(node, "anchor (#{match[:anchor]}) is missing in: #{path_to_file}") end - record_lint(node, "remove .md extension from the link: #{link}") if link.end_with?('.md') + record_lint(node, "add .md extension to the link: #{link}") unless link.end_with?('.md') end def extract_link_and_anchor(ast_tree) diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb index 2174caf239b..51dba8d1381 100644 --- a/lib/banzai/filter/base_sanitization_filter.rb +++ b/lib/banzai/filter/base_sanitization_filter.rb @@ -15,8 +15,6 @@ module Banzai include Gitlab::Utils::StrongMemoize extend Gitlab::Utils::SanitizeNodeLink - RENDER_TIMEOUT = 5.seconds - UNSAFE_PROTOCOLS = %w[data javascript vbscript].freeze def call @@ -57,9 +55,8 @@ module Banzai allowlist[:attributes]['img'].push('data-diagram-src') # Allow any protocol in `a` elements - # and then remove links with unsafe protocols + # and then remove links with unsafe protocols in SanitizeLinkFilter allowlist[:protocols].delete('a') - allowlist[:transformers].push(self.class.method(:sanitize_unsafe_links)) # Remove `rel` attribute from `a` elements allowlist[:transformers].push(self.class.remove_rel) diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 8c141dfec51..8561066ee55 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -2,10 +2,10 @@ module Banzai module Filter - # HTML Filter to modify the attributes of external links + # HTML Filter to modify the attributes of external links. + # This is considered a sanitization filter. class ExternalLinkFilter < HTML::Pipeline::Filter prepend Concerns::TimeoutFilterHandler - prepend Concerns::PipelineTimingCheck SCHEMES = ['http', 'https', nil].freeze RTLO = "\u202E" @@ -46,7 +46,7 @@ module Banzai # partial un-sanitized results. # It's ok to allow any following filters to run since this is safe HTML. def returned_timeout_value - HTML::Pipeline.parse(COMPLEX_MARKDOWN_MESSAGE) + HTML::Pipeline.parse(Banzai::Filter::SanitizeLinkFilter::TIMEOUT_MARKDOWN_MESSAGE) end # if this is a link to a proxied image, then `src` is already the correct diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 01f8210b485..6aad2613c86 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -77,6 +77,7 @@ module Banzai def sanitized_content_tag(name, content, options = {}) html = content_tag(name, content, options) node = Banzai::Filter::SanitizationFilter.new(html).call + node = Banzai::Filter::SanitizeLinkFilter.new(node).call node&.children&.first end diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb index 1489f4c98d9..c8e2932bfd7 100644 --- a/lib/banzai/filter/references/abstract_reference_filter.rb +++ b/lib/banzai/filter/references/abstract_reference_filter.rb @@ -10,8 +10,6 @@ module Banzai prepend Concerns::TimeoutFilterHandler prepend Concerns::PipelineTimingCheck - RENDER_TIMEOUT = 2.seconds - def initialize(doc, context = nil, result = nil) super diff --git a/lib/banzai/filter/sanitize_link_filter.rb b/lib/banzai/filter/sanitize_link_filter.rb new file mode 100644 index 00000000000..3c4682987b3 --- /dev/null +++ b/lib/banzai/filter/sanitize_link_filter.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # Validate links and remove unsafe protocols. + # This can be intensive, so it was split from BaseSanitizationFilter in order + # for it to have its own time period. + class SanitizeLinkFilter < HTML::Pipeline::Filter + prepend Concerns::TimeoutFilterHandler + include Gitlab::Utils::SanitizeNodeLink + + # [href], [src], [data-src], [data-canonical-src] + CSS = Gitlab::Utils::SanitizeNodeLink::ATTRS_TO_SANITIZE.map { |x| "[#{x}]" }.join(', ') + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + + TIMEOUT_MARKDOWN_MESSAGE = + <<~HTML +

Timeout while sanitizing links - rendering aborted. Please reduce the number of links if possible.

+ HTML + + def call + doc.xpath(self.class::XPATH).each do |el| + sanitize_unsafe_links({ node: el }) + end + + doc + end + + private + + def render_timeout + SANITIZATION_RENDER_TIMEOUT + end + + # If sanitization times out, we can not return partial un-sanitized results. + # It's ok to allow any following filters to run since this is safe HTML. + def returned_timeout_value + HTML::Pipeline.parse(TIMEOUT_MARKDOWN_MESSAGE) + end + end + end +end diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb index 479565a9f53..d8b4f447583 100644 --- a/lib/banzai/filter/spaced_link_filter.rb +++ b/lib/banzai/filter/spaced_link_filter.rb @@ -17,8 +17,8 @@ module Banzai # This is a small extension to the CommonMark spec. If they start allowing # spaces in urls, we could then remove this filter. # - # Note: Filter::SanitizationFilter should always be run sometime after this filter - # to prevent XSS attacks + # Note: Filter::SanitizationFilter/Filter::SanitizeLinkFilter should always be run sometime + # after this filter to prevent XSS attacks # class SpacedLinkFilter < HTML::Pipeline::Filter prepend Concerns::PipelineTimingCheck diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb index 0829d8dd556..97ceaad9e7d 100644 --- a/lib/banzai/pipeline/ascii_doc_pipeline.rb +++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb @@ -6,6 +6,7 @@ module Banzai def self.filters FilterArray[ Filter::AsciiDocSanitizationFilter, + Filter::SanitizeLinkFilter, Filter::CodeLanguageFilter, Filter::GollumTagsFilter, Filter::WikiLinkGollumFilter, diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb index 91a0723a965..813d5532310 100644 --- a/lib/banzai/pipeline/broadcast_message_pipeline.rb +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -8,6 +8,7 @@ module Banzai Filter::BlockquoteFenceLegacyFilter, Filter::MarkdownFilter, Filter::BroadcastMessageSanitizationFilter, + Filter::SanitizeLinkFilter, Filter::EmojiFilter, Filter::ColorFilter, Filter::AutolinkFilter, diff --git a/lib/banzai/pipeline/emoji_pipeline.rb b/lib/banzai/pipeline/emoji_pipeline.rb index a1b522f4303..a155b336271 100644 --- a/lib/banzai/pipeline/emoji_pipeline.rb +++ b/lib/banzai/pipeline/emoji_pipeline.rb @@ -9,6 +9,7 @@ module Banzai @filters ||= FilterArray[ Filter::HtmlEntityFilter, Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, Filter::EmojiFilter ] end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5d51037aae4..d1f46c51bad 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -13,9 +13,10 @@ module Banzai @filters ||= FilterArray[ Filter::CodeLanguageFilter, Filter::PlantumlFilter, - # Must always be before the SanitizationFilter to prevent XSS attacks + # Must always be before the SanitizationFilter/SanitizeLinkFilter to prevent XSS attacks Filter::SpacedLinkFilter, Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, Filter::EscapedCharFilter, Filter::KrokiFilter, Filter::GollumTagsFilter, diff --git a/lib/banzai/pipeline/incident_management/timeline_event_pipeline.rb b/lib/banzai/pipeline/incident_management/timeline_event_pipeline.rb index b00b4dd85b5..c347ef3da15 100644 --- a/lib/banzai/pipeline/incident_management/timeline_event_pipeline.rb +++ b/lib/banzai/pipeline/incident_management/timeline_event_pipeline.rb @@ -12,6 +12,7 @@ module Banzai @filters ||= FilterArray[ *super, Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, *Banzai::Pipeline::GfmPipeline.reference_filters, Filter::EmojiFilter, Filter::ExternalLinkFilter, diff --git a/lib/banzai/pipeline/label_pipeline.rb b/lib/banzai/pipeline/label_pipeline.rb index ccfda2052e6..7999ca3454e 100644 --- a/lib/banzai/pipeline/label_pipeline.rb +++ b/lib/banzai/pipeline/label_pipeline.rb @@ -6,6 +6,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, Filter::References::LabelReferenceFilter ] end diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb index cb421ff77b6..985000f24dd 100644 --- a/lib/banzai/pipeline/markup_pipeline.rb +++ b/lib/banzai/pipeline/markup_pipeline.rb @@ -6,6 +6,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, Filter::CodeLanguageFilter, Filter::AssetProxyFilter, Filter::ExternalLinkFilter, diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 0de9157a2b0..e42596861c7 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -7,6 +7,7 @@ module Banzai @filters ||= FilterArray[ Filter::HtmlEntityFilter, Filter::SanitizationFilter, + Filter::SanitizeLinkFilter, Filter::AssetProxyFilter, Filter::EmojiFilter, Filter::CustomEmojiFilter, diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb index de726b57053..d80323589dc 100644 --- a/lib/gitlab/ci/config/external/file/project.rb +++ b/lib/gitlab/ci/config/external/file/project.rb @@ -90,12 +90,16 @@ module Gitlab .batch(key: context.user) do |projects, loader, args| projects.uniq.each do |project| context.logger.instrument(:config_file_project_validate_access) do - loader.call(project, Ability.allowed?(args[:key], :download_code, project)) + loader.call(project, project_access_allowed?(args[:key], project)) end end end end + def project_access_allowed?(user, project) + Ability.allowed?(user, :download_code, project) + end + def sha return if project.nil? @@ -179,3 +183,5 @@ module Gitlab end end end + +Gitlab::Ci::Config::External::File::Project.prepend_mod diff --git a/lib/gitlab/ci/project_config.rb b/lib/gitlab/ci/project_config.rb index f92b84abfa9..bb185cf0a7d 100644 --- a/lib/gitlab/ci/project_config.rb +++ b/lib/gitlab/ci/project_config.rb @@ -49,7 +49,7 @@ module Gitlab end end - delegate :content, :source, :url, to: :@config, allow_nil: true + delegate :content, :source, :url, :pipeline_policy_context, to: :@config, allow_nil: true delegate :internal_include_prepended?, to: :@config def exists? diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb index 755640ad965..7288ee7fcd3 100644 --- a/lib/gitlab/ci/project_config/source.rb +++ b/lib/gitlab/ci/project_config/source.rb @@ -42,10 +42,12 @@ module Gitlab nil end + attr_reader :pipeline_policy_context + private attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge, :triggered_for_branch, - :ref, :pipeline_policy_context + :ref def ci_config_path @ci_config_path ||= project.ci_config_path_or_default diff --git a/lib/gitlab/experiment/rollout/feature.rb b/lib/gitlab/experiment/rollout/feature.rb deleted file mode 100644 index 0f2a1b9fb1d..00000000000 --- a/lib/gitlab/experiment/rollout/feature.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - class Experiment - module Rollout - class Feature < Percent - # For this rollout strategy to consider an experiment as enabled, we - # must: - # - # - have a feature flag yaml file that declares it. - # - be in an environment that permits it. - # - not have rolled out the feature flag at all (no percent of actors, - # no inclusions, etc.) - def enabled? - return false unless feature_flag_defined? - return false unless available? - return false unless ::Feature.enabled?(:gitlab_experiment, type: :ops) - - feature_flag_instance.state != :off - end - - # For assignment we first check to see if our feature flag is enabled - # for "self". This is done by calling `#flipper_id` (used behind the - # scenes by `Feature`). By default this is our `experiment.id` (or more - # specifically, the context key, which is an anonymous SHA generated - # using the details of an experiment. - # - # If the `Feature.enabled?` check is false, we return nil implicitly, - # which will assign the control. Otherwise we call super, which will - # assign a variant based on our provided distribution rules. - # Otherwise we will assign a variant evenly across the behaviours without control. - def execute_assignment - super if ::Feature.enabled?(feature_flag_name, self, type: :experiment) - end - - # This is what's provided to the `Feature.enabled?` call that will be - # used to determine experiment inclusion. An experiment may provide an - # override for this method to make the experiment work on user, group, - # or projects. - # - # For example, when running an experiment on a project, you could make - # the experiment assignable by project (using chatops) by implementing - # a `flipper_id` method in the experiment: - # - # def flipper_id - # context.project.flipper_id - # end - # - # Or even cleaner, simply delegate it: - # - # delegate :flipper_id, to: -> { context.project } - def flipper_id - return experiment.flipper_id if experiment.respond_to?(:flipper_id) - - "Experiment;#{id}" - end - - private - - def available? - ApplicationExperiment.available? - end - - def feature_flag_instance - ::Feature.get(feature_flag_name) # rubocop:disable Gitlab/AvoidFeatureGet -- We are using at a lower layer here in experiment framework - end - - def feature_flag_defined? - ::Feature::Definition.get(feature_flag_name).present? - end - - def feature_flag_name - experiment.name.tr('/', '_') - end - - def behavior_names - super - [:control] - end - end - end - end -end diff --git a/lib/gitlab/experiment_feature_rollout.rb b/lib/gitlab/experiment_feature_rollout.rb new file mode 100644 index 00000000000..fccc91d5109 --- /dev/null +++ b/lib/gitlab/experiment_feature_rollout.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Gitlab + class ExperimentFeatureRollout < Gitlab::Experiment::Rollout::Percent + # For this rollout strategy to consider an experiment as enabled, we + # must: + # + # - have a feature flag yaml file that declares it. + # - be in an environment that permits it. + # - not have rolled out the feature flag at all (no percent of actors, + # no inclusions, etc.) + def enabled? + return false unless feature_flag_defined? + return false unless available? + return false unless ::Feature.enabled?(:gitlab_experiment, type: :ops) + + feature_flag_instance.state != :off + end + + # For assignment we first check to see if our feature flag is enabled + # for "self". This is done by calling `#flipper_id` (used behind the + # scenes by `Feature`). By default this is our `experiment.id` (or more + # specifically, the context key, which is an anonymous SHA generated + # using the details of an experiment. + # + # If the `Feature.enabled?` check is false, we return nil implicitly, + # which will assign the control. Otherwise we call super, which will + # assign a variant based on our provided distribution rules. + # Otherwise we will assign a variant evenly across the behaviours without control. + def execute_assignment + super if ::Feature.enabled?(feature_flag_name, self, type: :experiment) + end + + # This is what's provided to the `Feature.enabled?` call that will be + # used to determine experiment inclusion. An experiment may provide an + # override for this method to make the experiment work on user, group, + # or projects. + # + # For example, when running an experiment on a project, you could make + # the experiment assignable by project (using chatops) by implementing + # a `flipper_id` method in the experiment: + # + # def flipper_id + # context.project.flipper_id + # end + # + # Or even cleaner, simply delegate it: + # + # delegate :flipper_id, to: -> { context.project } + def flipper_id + return experiment.flipper_id if experiment.respond_to?(:flipper_id) + + "Experiment;#{id}" + end + + private + + def available? + ApplicationExperiment.available? + end + + def feature_flag_instance + ::Feature.get(feature_flag_name) # rubocop:disable Gitlab/AvoidFeatureGet -- We are using at a lower layer here in experiment framework + end + + def feature_flag_defined? + ::Feature::Definition.get(feature_flag_name).present? + end + + def feature_flag_name + experiment.name.tr('/', '_') + end + + def behavior_names + super - [:control] + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index a1787656171..3f9ba7c5db4 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -38,6 +38,8 @@ module Gitlab instance.milestone_url(object, **options) when Note note_url(object, **options) + when AntiAbuse::Reports::Note + abuse_report_note_url(object, **options) when Release instance.release_url(object, **options) when Organizations::Organization @@ -102,6 +104,10 @@ module Gitlab end end + def abuse_report_note_url(note, **options) + instance.admin_abuse_report_url(note.abuse_report, anchor: dom_id(note), **options) + end + def snippet_url(snippet, **options) if options[:file].present? file, ref = options.values_at(:file, :ref) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 10e4f4d6375..988f7e17ec9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2276,9 +2276,6 @@ msgstr "" msgid "AI|Explain with AI" msgstr "" -msgid "AI|Explain your rating to help us improve! (optional)" -msgstr "" - msgid "AI|For example: Organizations should be able to forecast into the future by using value stream analytics charts. This feature would help them understand how their metrics are trending." msgstr "" @@ -13655,7 +13652,7 @@ msgstr "" msgid "Commit message" msgstr "" -msgid "Commit message generated by AI" +msgid "Commit message generated by GitLab Duo" msgstr "" msgid "Commit statistics for %{ref} %{start_time} - %{end_time}" @@ -60375,9 +60372,6 @@ msgstr "" msgid "Vulnerability|Comments" msgstr "" -msgid "Vulnerability|Could not load prompt." -msgstr "" - msgid "Vulnerability|Crash address:" msgstr "" @@ -60408,9 +60402,6 @@ msgstr "" msgid "Vulnerability|Evidence:" msgstr "" -msgid "Vulnerability|Explain this vulnerability" -msgstr "" - msgid "Vulnerability|External Security Report" msgstr "" @@ -60423,12 +60414,6 @@ msgstr "" msgid "Vulnerability|GitLab Security Report" msgstr "" -msgid "Vulnerability|GitLab has identified sensitive strings in the code snippet for the AI prompt, indicating a possible leaked secret. Please review your code before utilizing the Explain This Vulnerability feature. If you still wish to proceed and send the %{linkStart}code%{linkEnd} to the AI, click the checkbox below." -msgstr "" - -msgid "Vulnerability|Hide prompt" -msgstr "" - msgid "Vulnerability|Identifier" msgstr "" @@ -60462,9 +60447,6 @@ msgstr "" msgid "Vulnerability|Project:" msgstr "" -msgid "Vulnerability|Providing the source code improves the response quality. If security is a concern, you can send basic vulnerability info for a generic example." -msgstr "" - msgid "Vulnerability|Remove identifier row" msgstr "" @@ -60474,9 +60456,6 @@ msgstr "" msgid "Vulnerability|Request/Response" msgstr "" -msgid "Vulnerability|Response generated by AI" -msgstr "" - msgid "Vulnerability|Scanner:" msgstr "" @@ -60489,12 +60468,6 @@ msgstr "" msgid "Vulnerability|Select a severity" msgstr "" -msgid "Vulnerability|Send code with prompt" -msgstr "" - -msgid "Vulnerability|Sending code to AI" -msgstr "" - msgid "Vulnerability|Sent request:" msgstr "" @@ -60507,9 +60480,6 @@ msgstr "" msgid "Vulnerability|Severity:" msgstr "" -msgid "Vulnerability|Show prompt" -msgstr "" - msgid "Vulnerability|Something went wrong while trying to get the source file." msgstr "" @@ -60558,21 +60528,12 @@ msgstr "" msgid "Vulnerability|Vulnerable method:" msgstr "" -msgid "Vulnerability|Warning: possible secrets detected" -msgstr "" - msgid "Vulnerability|What is code flow?" msgstr "" -msgid "Vulnerability|You can also %{message}." -msgstr "" - msgid "Vulnerability|code_flow" msgstr "" -msgid "Vulnerability|use AI by asking GitLab Duo Chat to explain this vulnerability and suggest a solution" -msgstr "" - msgid "WARNING:" msgstr "" diff --git a/package.json b/package.json index 7d741b4bc93..542a9a878f9 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@gitlab/query-language": "^0.0.5-a-20240903", "@gitlab/svgs": "3.112.0", "@gitlab/ui": "91.1.2", - "@gitlab/web-ide": "^0.0.1-dev-20240816130114", + "@gitlab/web-ide": "^0.0.1-dev-20240909013227", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "7.0.8-4", "@rails/ujs": "7.0.8-4", diff --git a/patches/@gitlab+web-ide+0.0.1-dev-20240816130114.patch b/patches/@gitlab+web-ide+0.0.1-dev-20240909013227.patch similarity index 99% rename from patches/@gitlab+web-ide+0.0.1-dev-20240816130114.patch rename to patches/@gitlab+web-ide+0.0.1-dev-20240909013227.patch index 0b40e9ec7d4..815bfb36eeb 100644 --- a/patches/@gitlab+web-ide+0.0.1-dev-20240816130114.patch +++ b/patches/@gitlab+web-ide+0.0.1-dev-20240909013227.patch @@ -2950,5 +2950,5 @@ index 6a16dd1..99b1df4 100644 - const parentOrigin = searchParams.get('parentOrigin') || window.origin; + const parentOrigin = window.origin; const salt = searchParams.get('salt'); - + (async function () { diff --git a/spec/factories/integrations/instance_integrations.rb b/spec/factories/integrations/instance_integrations.rb new file mode 100644 index 00000000000..d70b7ecf889 --- /dev/null +++ b/spec/factories/integrations/instance_integrations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :instance_integration, class: 'Integrations::InstanceIntegration' do + type { 'Integrations::InstanceIntegration' } + end +end diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb index e12b634828e..fa87c7dc25e 100644 --- a/spec/features/profiles/two_factor_auths_spec.rb +++ b/spec/features/profiles/two_factor_auths_spec.rb @@ -59,7 +59,7 @@ RSpec.describe 'Two factor auths', feature_category: :system_access do fill_in 'pin_code', with: '123' click_button 'Register with two-factor app' - expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication_troubleshooting')) + expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication_troubleshooting.md')) end end diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js index bc7aa8ef5de..d1f5c3bbb17 100644 --- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js +++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js @@ -93,21 +93,8 @@ describe('Abuse Report Note', () => { }); describe('Editing', () => { - it('should show edit button when resolveNote is true', () => { - createComponent({ - note: { ...mockNote, userPermissions: { resolveNote: true } }, - }); - - expect(findNoteActions().props()).toMatchObject({ - showEditButton: true, - }); - }); - - it('should not show edit button when resolveNote is false', () => { - createComponent({ - note: { ...mockNote, userPermissions: { resolveNote: false } }, - }); - + // this should be changed: https://gitlab.com/gitlab-org/gitlab/-/issues/481897 + it('should not show edit button', () => { expect(findNoteActions().props()).toMatchObject({ showEditButton: false, }); diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js index 0f37ef218d2..8dafbcc9528 100644 --- a/spec/frontend/admin/abuse_report/mock_data.js +++ b/spec/frontend/admin/abuse_report/mock_data.js @@ -318,6 +318,171 @@ export const mockDiscussionWithReplies = [ }, ]; +export const mockAbuseReportDiscussionWithNoReplies = [ + { + id: 'gid://gitlab/AntiAbuse::Reports::Note/1', + body: 'Comment 1', + bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 1\u003c/p\u003e', + createdAt: '2023-10-19T06:11:13Z', + lastEditedAt: null, + url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_1', + resolved: false, + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + webPath: '/root', + __typename: 'UserCore', + }, + lastEditedBy: null, + discussion: { + id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d', + notes: { + nodes: [ + { + id: 'gid://gitlab/AntiAbuse::Reports::Note/1', + __typename: 'Note', + }, + ], + __typename: 'AbuseReportNoteConnection', + }, + __typename: 'AbuseReportDiscussion', + }, + __typename: 'AbuseReportNote', + }, +]; +export const mockAbuseReportDiscussionWithReplies = [ + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/2', + body: 'Comment 2', + bodyHtml: '\u003cp data-sourcepos="1:1-1:9" dir="auto"\u003eComment 2\u003c/p\u003e', + createdAt: '2023-10-20T07:47:21Z', + lastEditedAt: '2023-10-20T07:47:42Z', + url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_2', + resolved: false, + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + webPath: '/root', + __typename: 'UserCore', + }, + lastEditedBy: null, + discussion: { + id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', + notes: { + nodes: [ + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/2', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/3', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/4', + __typename: 'AbuseReportNote', + }, + ], + __typename: 'AbuseReportNoteConnection', + }, + __typename: 'AbuseReportDiscussion', + }, + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/3', + body: 'Reply comment 1', + bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 1\u003c/p\u003e', + createdAt: '2023-10-20T07:47:42Z', + lastEditedAt: '2023-10-20T07:47:42Z', + url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_3', + resolved: false, + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + webPath: '/root', + __typename: 'UserCore', + }, + lastEditedBy: null, + discussion: { + id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', + notes: { + nodes: [ + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/2', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/3', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/4', + __typename: 'AbuseReportNote', + }, + ], + __typename: 'AbuseReportNoteConnection', + }, + __typename: 'AbuseReportDiscussion', + }, + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/4', + body: 'Reply comment 2', + bodyHtml: '\u003cp data-sourcepos="1:1-1:15" dir="auto"\u003eReply comment 2\u003c/p\u003e', + createdAt: '2023-10-20T08:26:51Z', + lastEditedAt: '2023-10-20T08:26:51Z', + url: 'http://127.0.0.1:3000/admin/abuse_reports/1#note_4', + resolved: false, + author: { + id: 'gid://gitlab/User/1', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + name: 'Administrator', + username: 'root', + webUrl: 'http://127.0.0.1:3000/root', + webPath: '/root', + __typename: 'UserCore', + }, + lastEditedBy: null, + discussion: { + id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', + notes: { + nodes: [ + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/2', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/3', + __typename: 'AbuseReportNote', + }, + { + id: 'gid://gitlab/AntiAbuse::Reports::DiscussionNote/4', + __typename: 'AbuseReportNote', + }, + ], + __typename: 'AbuseReportNoteConnection', + }, + __typename: 'AbuseReportDiscussion', + }, + __typename: 'AbuseReportNote', + }, +]; + export const mockNotesByIdResponse = { data: { abuseReport: { @@ -327,22 +492,23 @@ export const mockNotesByIdResponse = { { id: 'gid://gitlab/Discussion/055af96ab917175219aec8739c911277b18ea41d', replyId: - 'gid://gitlab/IndividualNoteDiscussion/055af96ab917175219aec8739c911277b18ea41d', + 'gid://gitlab/AntiAbuse::Reports::IndividualNoteDiscussion/055af96ab917175219aec8739c911277b18ea41d', notes: { - nodes: mockDiscussionWithNoReplies, - __typename: 'NoteConnection', + nodes: mockAbuseReportDiscussionWithNoReplies, + __typename: 'AbuseReportNoteConnection', }, }, { id: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', - replyId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', + replyId: + 'gid://gitlab/AntiAbuse::Reports::Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a', notes: { - nodes: mockDiscussionWithReplies, - __typename: 'NoteConnection', + nodes: mockAbuseReportDiscussionWithReplies, + __typename: 'AbuseReportNoteConnection', }, }, ], - __typename: 'DiscussionConnection', + __typename: 'AbuseReportDiscussionConnection', }, __typename: 'AbuseReport', }, diff --git a/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js index d204576fe8f..e1f6c65acce 100644 --- a/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js +++ b/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js @@ -1,4 +1,4 @@ -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; @@ -10,7 +10,9 @@ import ActionComponent from '~/ci/common/private/job_action_component.vue'; describe('pipeline graph action component', () => { let wrapper; let mock; + const findButton = () => wrapper.findComponent(GlButton); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const defaultProps = { tooltipText: 'bar', @@ -69,11 +71,21 @@ describe('pipeline graph action component', () => { expect(wrapper.emitted().pipelineActionRequestComplete).toHaveLength(1); }); - it('renders a loading icon while waiting for request', async () => { + it('displays a loading icon/disabled button while waiting for request', async () => { + expect(findLoadingIcon().exists()).toBe(false); + expect(findButton().props('disabled')).toBe(false); + findButton().trigger('click'); await nextTick(); - expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findButton().props('disabled')).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + expect(findButton().props('disabled')).toBe(false); }); }); diff --git a/spec/frontend/ide/web_ide_assets_spec.js b/spec/frontend/ide/web_ide_assets_spec.js index d59b09f4cbc..692c7f9853c 100644 --- a/spec/frontend/ide/web_ide_assets_spec.js +++ b/spec/frontend/ide/web_ide_assets_spec.js @@ -25,6 +25,24 @@ describe('asset patching in @gitlab/web-ide', () => { }); const htmlChildren = allChildren.filter((x) => x.endsWith('.html')); + /** + * ## What in the world is this test doing!? + * + * This test was introduced when we were fixing a [security vulnerability][1] related to GitLab self-hosting + * problematic `.html` files. These files could be exploited through an `iframe` on an `evil.com` and will + * assume the user's cookie authentication. Boom! + * + * ## How do I know if an `.html` file is vulnerable? + * + * - The `.html` file used the `postMessage` API and allowed any `origin` which enabled any external site to + * open it in an `iframe` and communicate to it. + * - The `iframe` exposed some internal VSCode message bus that could allow arbitrary requests. So watch out for + * `fetch`. + * + * [1]: https://gitlab.com/gitlab-org/security/gitlab-web-ide-vscode-fork/-/issues/1#note_1905417620 + * + * ========== If expectation fails and you can't see the full comment... LOOK UP! ============== + */ expect(htmlChildren).toEqual([ // This is the only HTML file we expect and it's protected by the other test. 'out/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', @@ -33,6 +51,9 @@ describe('asset patching in @gitlab/web-ide', () => { 'extensions/microsoft-authentication/media/index.html', 'extensions/gitlab-vscode-extension/webviews/security_finding/index.html', 'extensions/gitlab-vscode-extension/webviews/gitlab_duo_chat/index.html', + 'extensions/gitlab-vscode-extension/assets/language-server/webviews/duo-workflow/index.html', + 'extensions/gitlab-vscode-extension/assets/language-server/webviews/duo-chat/index.html', + 'extensions/gitlab-vscode-extension/assets/language-server/webviews/chat/index.html', 'extensions/github-authentication/media/index.html', ]); }); diff --git a/spec/graphql/types/abuse_report_type_spec.rb b/spec/graphql/types/abuse_report_type_spec.rb new file mode 100644 index 00000000000..6ee1aad12d8 --- /dev/null +++ b/spec/graphql/types/abuse_report_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['AbuseReport'], feature_category: :insider_threat do + let(:fields) { %w[id labels discussions notes] } + + specify { expect(described_class.graphql_name).to eq('AbuseReport') } + + specify { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/spec/graphql/types/notes/abuse_report/discussion_type_spec.rb b/spec/graphql/types/notes/abuse_report/discussion_type_spec.rb new file mode 100644 index 00000000000..c97844881ab --- /dev/null +++ b/spec/graphql/types/notes/abuse_report/discussion_type_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['AbuseReportDiscussion'], feature_category: :team_planning do + include GraphqlHelpers + + it 'exposes the expected fields' do + expected_fields = %i[ + abuse_report + created_at + id + notes + reply_id + resolvable + resolved + resolved_at + resolved_by + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + specify { expect(described_class.graphql_name).to eq('AbuseReportDiscussion') } +end diff --git a/spec/graphql/types/notes/abuse_report/note_type_spec.rb b/spec/graphql/types/notes/abuse_report/note_type_spec.rb new file mode 100644 index 00000000000..3945e1906a2 --- /dev/null +++ b/spec/graphql/types/notes/abuse_report/note_type_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['AbuseReportNote'], feature_category: :team_planning do + include GraphqlHelpers + + it 'exposes the expected fields' do + expected_fields = %i[ + author + body + body_html + body_first_line_html + award_emoji + created_at + discussion + id + resolvable + resolved + resolved_at + resolved_by + updated_at + url + last_edited_at + last_edited_by + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + specify { expect(described_class.graphql_name).to eq('AbuseReportNote') } +end diff --git a/spec/graphql/types/notes/note_type_spec.rb b/spec/graphql/types/notes/note_type_spec.rb index dde51d84f78..d16cf5f44a7 100644 --- a/spec/graphql/types/notes/note_type_spec.rb +++ b/spec/graphql/types/notes/note_type_spec.rb @@ -5,6 +5,16 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Note'], feature_category: :team_planning do include GraphqlHelpers + # rubocop:disable RSpec/FactoryBot/AvoidCreate -- we need the project and author for the test, id needed + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { build_stubbed(:user) } + + let_it_be(:note_text) { 'note body content' } + let_it_be(:note) { create(:note, note: note_text, project: project) } + # rubocop:enable RSpec/FactoryBot/AvoidCreate + + let(:batch_loader) { instance_double(Gitlab::Graphql::Loaders::BatchModelLoader) } + it 'exposes the expected fields' do expected_fields = %i[ author @@ -42,12 +52,10 @@ RSpec.describe GitlabSchema.types['Note'], feature_category: :team_planning do specify { expect(described_class).to require_graphql_authorizations(:read_note) } context 'when system note with issue_email_participants action', feature_category: :service_desk do - let_it_be(:user) { build_stubbed(:user) } let_it_be(:email) { 'user@example.com' } let_it_be(:note_text) { "added #{email}" } # Create project and issue separately because we need to public project. # rubocop:disable RSpec/FactoryBot/AvoidCreate -- Notes::RenderService updates #note and #cached_markdown_version - let_it_be(:project) { create(:project, :public) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:note) do create(:note, :system, project: project, noteable: issue, author: Users::Internal.support_bot, note: note_text) @@ -72,9 +80,6 @@ RSpec.describe GitlabSchema.types['Note'], feature_category: :team_planning do end describe '#body_first_line_html' do - let_it_be(:user) { build_stubbed(:user) } - let_it_be(:project) { build(:project, :public) } - let(:note_text) { 'note body content' } let(:note) { build(:note, note: note_text, project: project) } @@ -109,4 +114,28 @@ RSpec.describe GitlabSchema.types['Note'], feature_category: :team_planning do end end end + + describe '#project' do + subject(:note_project) { resolve_field(:project, note, current_user: user) } + + it 'fetches the project' do + expect(Gitlab::Graphql::Loaders::BatchModelLoader).to receive(:new).with(Project, project.id) + .and_return(batch_loader) + expect(batch_loader).to receive(:find) + + note_project + end + end + + describe '#author' do + subject(:note_author) { resolve_field(:author, note, current_user: user) } + + it 'fetches the author' do + expect(Gitlab::Graphql::Loaders::BatchModelLoader).to receive(:new).with(User, note.author.id) + .and_return(batch_loader) + expect(batch_loader).to receive(:find) + + note_author + end + end end diff --git a/spec/graphql/types/notes/noteable_interface_spec.rb b/spec/graphql/types/notes/noteable_interface_spec.rb index c88cfe18b81..e11dece60b8 100644 --- a/spec/graphql/types/notes/noteable_interface_spec.rb +++ b/spec/graphql/types/notes/noteable_interface_spec.rb @@ -19,7 +19,6 @@ RSpec.describe Types::Notes::NoteableInterface do expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType) expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType) expect(described_class.resolve_type(build(:alert_management_alert), {})).to eq(Types::AlertManagement::AlertType) - expect(described_class.resolve_type(build(:abuse_report), {})).to eq(Types::AbuseReportType) end end end diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb index 0b3c93306db..f1a7e4146be 100644 --- a/spec/haml_lint/linter/documentation_links_spec.rb +++ b/spec/haml_lint/linter/documentation_links_spec.rb @@ -11,49 +11,49 @@ RSpec.describe HamlLint::Linter::DocumentationLinks, feature_category: :tooling shared_examples 'link validation rules' do |link_pattern| context 'when link_to points to the existing file path' do - let(:haml) { "= link_to 'Description', #{link_pattern}('index')" } + let(:haml) { "= link_to 'Description', #{link_pattern}('index.md')" } it { is_expected.not_to report_lint } end context 'when link_to points to the existing file with valid anchor' do - let(:haml) { "= link_to 'Description', #{link_pattern}('index', anchor: 'user-accounts'), target: '_blank'" } + let(:haml) { "= link_to 'Description', #{link_pattern}('index.md', anchor: 'user-accounts'), target: '_blank'" } it { is_expected.not_to report_lint } end - context 'when link_to points to the existing file path with .md extension' do - let(:haml) { "= link_to 'Description', #{link_pattern}('index.md')" } + context 'when link_to points to the existing file path without .md extension' do + let(:haml) { "= link_to 'Description', #{link_pattern}('index')" } it { is_expected.to report_lint } end context 'when anchor is not correct' do - let(:haml) { "= link_to 'Description', #{link_pattern}('index', anchor: 'wrong')" } + let(:haml) { "= link_to 'Description', #{link_pattern}('index.md', anchor: 'wrong')" } it { is_expected.to report_lint } context "when #{link_pattern} has multiple options" do - let(:haml) { "= link_to 'Description', #{link_pattern}('index', key: :value, anchor: 'wrong')" } + let(:haml) { "= link_to 'Description', #{link_pattern}('index.md', key: :value, anchor: 'wrong')" } it { is_expected.to report_lint } end end context 'when file path is wrong' do - let(:haml) { "= link_to 'Description', #{link_pattern}('wrong'), target: '_blank'" } + let(:haml) { "= link_to 'Description', #{link_pattern}('wrong.md'), target: '_blank'" } it { is_expected.to report_lint } context 'when haml ends with block definition' do - let(:haml) { "= link_to 'Description', #{link_pattern}('wrong') do" } + let(:haml) { "= link_to 'Description', #{link_pattern}('wrong.md') do" } it { is_expected.to report_lint } end end context 'when link with wrong file path is assigned to a variable' do - let(:haml) { "- my_link = link_to 'Description', #{link_pattern}('wrong')" } + let(:haml) { "- my_link = link_to 'Description', #{link_pattern}('wrong.md')" } it { is_expected.to report_lint } end @@ -65,7 +65,7 @@ RSpec.describe HamlLint::Linter::DocumentationLinks, feature_category: :tooling end context 'when anchor belongs to a different element' do - let(:haml) { "= link_to 'Description', #{link_pattern}('index'), target: (anchor: 'blank')" } + let(:haml) { "= link_to 'Description', #{link_pattern}('index.md'), target: (anchor: 'blank')" } it { is_expected.not_to report_lint } end @@ -89,7 +89,7 @@ RSpec.describe HamlLint::Linter::DocumentationLinks, feature_category: :tooling end context 'when the second link is invalid' do - let(:haml) { ".data-form{ data: { url: #{link_pattern}('index'), wrong_url: #{link_pattern}('wrong') } }" } + let(:haml) { ".data-form{ data: { url: #{link_pattern}('index.md'), wrong_url: #{link_pattern}('wrong') } }" } it { is_expected.to report_lint } end diff --git a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb index 69afddf2406..d8a98d47356 100644 --- a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb @@ -8,9 +8,6 @@ RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter, feature_categ it_behaves_like 'default allowlist' describe 'custom allowlist' do - it_behaves_like 'XSS prevention' - it_behaves_like 'sanitize link' - subject { filter(exp).to_html } context 'allows `a` elements' do diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 0ba1dc9ed04..5e05a5cdabc 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -210,10 +210,11 @@ RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_plann end end - it_behaves_like 'pipeline timing check' + it_behaves_like 'does not use pipeline timing check' + it_behaves_like 'a filter timeout' do let(:text) { 'text' } - let(:expected_result) { described_class::COMPLEX_MARKDOWN_MESSAGE } + let(:expected_result) { Banzai::Filter::SanitizeLinkFilter::TIMEOUT_MARKDOWN_MESSAGE } let(:expected_timeout) { described_class::SANITIZATION_RENDER_TIMEOUT } end end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index bef26969517..4a6f22d3813 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -314,6 +314,7 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context) doc = Banzai::Filter::CodeLanguageFilter.call(doc[:output], context, nil) doc = Banzai::Filter::SanitizationFilter.call(doc, context, nil) + doc = Banzai::Filter::SanitizeLinkFilter.call(doc, context, nil) filter(doc, context) end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 7228ff5a95c..878f24ddaf2 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -8,9 +8,6 @@ RSpec.describe Banzai::Filter::SanitizationFilter, feature_category: :team_plann it_behaves_like 'default allowlist' describe 'custom allowlist' do - it_behaves_like 'XSS prevention' - it_behaves_like 'sanitize link' - it 'customizes the allowlist only once' do instance = described_class.new('Foo') control_count = instance.allowlist[:transformers].size @@ -224,6 +221,8 @@ RSpec.describe Banzai::Filter::SanitizationFilter, feature_category: :team_plann end end + it_behaves_like 'does not use pipeline timing check' + it_behaves_like 'a filter timeout' do let(:text) { 'text' } let(:expected_result) { described_class::COMPLEX_MARKDOWN_MESSAGE } diff --git a/spec/lib/banzai/filter/sanitize_link_filter_spec.rb b/spec/lib/banzai/filter/sanitize_link_filter_spec.rb new file mode 100644 index 00000000000..bc867b53958 --- /dev/null +++ b/spec/lib/banzai/filter/sanitize_link_filter_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::SanitizeLinkFilter, feature_category: :team_planning do + include FilterSpecHelper + + it_behaves_like 'XSS prevention' + it_behaves_like 'sanitize link' + it_behaves_like 'does not use pipeline timing check' + + it_behaves_like 'a filter timeout' do + let(:text) { 'text' } + let(:expected_result) { described_class::TIMEOUT_MARKDOWN_MESSAGE } + let(:expected_timeout) { described_class::SANITIZATION_RENDER_TIMEOUT } + end +end diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb index 9f1af821d11..f186acd7062 100644 --- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb @@ -12,6 +12,8 @@ RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline, feature_category: :te subject { described_class.to_html(exp, project: project) } + it_behaves_like 'sanitize pipeline' + context "allows `a` elements" do let(:exp) { "Link" } diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb index 2d831d7f7e0..67f5123fbbf 100644 --- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -21,6 +21,8 @@ RSpec.describe Banzai::Pipeline::DescriptionPipeline, feature_category: :team_pl stub_commonmark_sourcepos_disabled end + it_behaves_like 'sanitize pipeline' + it 'uses a limited allowlist' do doc = parse('# Description') diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb index 4d2dca84b1b..8b4abbd2ba5 100644 --- a/spec/lib/banzai/pipeline/email_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::EmailPipeline, feature_category: :team_planning do describe '.filters' do + it_behaves_like 'sanitize pipeline' + it 'returns the expected type' do expect(described_class.filters).to be_kind_of(Banzai::FilterArray) end diff --git a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb index 6ecd7f56dec..4680a4e525d 100644 --- a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb @@ -9,6 +9,8 @@ RSpec.describe Banzai::Pipeline::EmojiPipeline, feature_category: :team_planning described_class.to_html(text, {}) end + it_behaves_like 'sanitize pipeline' + it 'replaces emoji' do expected_result = "Hello world #{Gitlab::Emoji.gl_emoji_tag(emoji)}" diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index 11d5f202cb7..ed4e9faa6dd 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning do using RSpec::Parameterized::TableSyntax + it_behaves_like 'sanitize pipeline' + describe 'References' do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index 908f29c3555..ad90c67e422 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::GfmPipeline, feature_category: :team_planning do + it_behaves_like 'sanitize pipeline' + describe 'integration between parsing regular and external issue references' do let_it_be(:project) { create(:project, :with_redmine_integration, :public) } diff --git a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb index 9e79be4333a..bd3fa6041bc 100644 --- a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb @@ -5,12 +5,15 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do let_it_be(:project) { create(:project) } + it_behaves_like 'sanitize pipeline' + describe '.filters' do it 'contains required filters' do expect(described_class.filters).to eq( [ *Banzai::Pipeline::PlainMarkdownPipeline.filters, Banzai::Filter::SanitizationFilter, + Banzai::Filter::SanitizeLinkFilter, *Banzai::Pipeline::GfmPipeline.reference_filters, Banzai::Filter::EmojiFilter, Banzai::Filter::ExternalLinkFilter, diff --git a/spec/lib/banzai/pipeline/service_desk_email_pipeline_spec.rb b/spec/lib/banzai/pipeline/service_desk_email_pipeline_spec.rb index 83541494f68..886921d9a9e 100644 --- a/spec/lib/banzai/pipeline/service_desk_email_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/service_desk_email_pipeline_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::ServiceDeskEmailPipeline, feature_category: :service_desk do + it_behaves_like 'sanitize pipeline' + describe '.filters' do it 'returns the expected type' do expect(described_class.filters).to be_kind_of(Banzai::FilterArray) diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 423378bbd21..b9b386f244c 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -8,6 +8,8 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do let_it_be(:wiki) { ProjectWiki.new(project, nil) } let_it_be(:page) { build(:wiki_page, wiki: wiki, title: 'nested/twice/start-page') } + it_behaves_like 'sanitize pipeline' + describe 'TableOfContents' do it 'replaces the tag with the TableOfContentsTagFilter result' do markdown = <<-MD.strip_heredoc diff --git a/spec/lib/gitlab/experiment/rollout/feature_spec.rb b/spec/lib/gitlab/experiment_feature_rollout_spec.rb similarity index 97% rename from spec/lib/gitlab/experiment/rollout/feature_spec.rb rename to spec/lib/gitlab/experiment_feature_rollout_spec.rb index 9d602083ad6..62eddbfd5f9 100644 --- a/spec/lib/gitlab/experiment/rollout/feature_spec.rb +++ b/spec/lib/gitlab/experiment_feature_rollout_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Experiment::Rollout::Feature, :experiment, feature_category: :acquisition do +RSpec.describe Gitlab::ExperimentFeatureRollout, :experiment, feature_category: :acquisition do subject(:experiment_instance) { described_class.new(subject_experiment) } let(:subject_experiment) { experiment('namespaced/stub') } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index baa29e4e3ce..54ce7055be4 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -1140,7 +1140,7 @@ approval_rules: - approval_project_rules_users - approval_project_rules_protected_branches - scan_result_policy_read - - approval_policy_rules + - approval_policy_rule approval_project_rules_users: - user - approval_project_rule diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 420d4ad0197..f33826058ab 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -64,6 +64,7 @@ RSpec.describe Gitlab::UrlBuilder do :package | ->(package) { "/#{package.project.full_path}/-/packages/#{package.id}" } :user_namespace | ->(user_namespace) { "/#{user_namespace.owner.full_path}" } :project_namespace | ->(project_namespace) { "/#{project_namespace.project.full_path}" } + :abuse_report_note | ->(note) { "/admin/abuse_reports/#{note.abuse_report_id}#anti_abuse_reports_note_#{note.id}" } end with_them do diff --git a/spec/models/anti_abuse/reports/note_spec.rb b/spec/models/anti_abuse/reports/note_spec.rb index d8a4192f235..c0da5bb4601 100644 --- a/spec/models/anti_abuse/reports/note_spec.rb +++ b/spec/models/anti_abuse/reports/note_spec.rb @@ -32,5 +32,23 @@ RSpec.describe AntiAbuse::Reports::Note, feature_category: :insider_threat do expect(note1.note_html).to include('some note

') end end + + describe 'Scopes' do + describe '.inc_relations_for_view' do + subject(:incl_relations) { described_class.all.inc_relations_for_view.first } + + it 'loads associations' do + expect(incl_relations.association(:author).loaded?).to be(true) + expect(incl_relations.association(:updated_by).loaded?).to be(true) + expect(incl_relations.association(:award_emoji).loaded?).to be(true) + end + end + end + + describe '#parent_object_field' do + it 'returns the correct value' do + expect(described_class.parent_object_field).to eq(:abuse_report) + end + end end end diff --git a/spec/requests/api/graphql/abuse_report_spec.rb b/spec/requests/api/graphql/abuse_report_spec.rb index 5ffa3588c67..2596a247f68 100644 --- a/spec/requests/api/graphql/abuse_report_spec.rb +++ b/spec/requests/api/graphql/abuse_report_spec.rb @@ -66,4 +66,33 @@ RSpec.describe 'Querying an Abuse Report', feature_category: :insider_threat do ) end end + + describe 'notes' do + let_it_be(:abuse_report) { create(:abuse_report) } + let_it_be(:note_1) { create(:abuse_report_note, abuse_report: abuse_report) } + let_it_be(:note_2) { create(:abuse_report_note, abuse_report: abuse_report) } + let_it_be(:reply) { create(:abuse_report_note, in_reply_to: note_1) } + + let(:notes_response) do + graphql_data_at(:abuse_report, :notes, :nodes) + end + + let(:abuse_report_fields) do + <<~GRAPHQL + notes { + nodes { + id + body + bodyHtml + } + } + GRAPHQL + end + + it 'returns notes' do + expect(notes_response).to match_array( + [a_graphql_entity_for(note_1), a_graphql_entity_for(note_2), a_graphql_entity_for(reply)] + ) + end + end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index f4599f03a3e..7a55b8e7416 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -190,6 +190,7 @@ project_setting: - duo_features_enabled - require_reauthentication_to_approve - observability_alerts_enabled + - spp_repository_pipeline_access build_service_desk_setting: # service_desk_setting unexposed_attributes: diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 89054a15a79..bedb7dcd263 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -5176,7 +5176,7 @@ - './spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb' - './spec/lib/gitlab/exclusive_lease_helpers_spec.rb' - './spec/lib/gitlab/exclusive_lease_spec.rb' -- './spec/lib/gitlab/experiment/rollout/feature_spec.rb' +- './spec/lib/gitlab/experiment_feature_rollout_spec.rb' - './spec/lib/gitlab/external_authorization/access_spec.rb' - './spec/lib/gitlab/external_authorization/cache_spec.rb' - './spec/lib/gitlab/external_authorization/client_spec.rb' diff --git a/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb index 6927b2d7a6d..4074745cfbc 100644 --- a/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb +++ b/spec/support/shared_examples/banzai/filters/filter_timeout_shared_examples.rb @@ -75,6 +75,15 @@ RSpec.shared_examples 'pipeline timing check' do |context: {}| end end +# Usage: +# +# it_behaves_like 'does not use pipeline timing check' +RSpec.shared_examples 'does not use pipeline timing check' do + it 'does not include Concerns::PipelineTimingCheck' do + expect(described_class).not_to include Banzai::Filter::Concerns::PipelineTimingCheck + end +end + # Usage: # # it_behaves_like 'limits the number of filtered items' do diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb index e335255e426..6a06472e386 100644 --- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb @@ -13,18 +13,6 @@ RSpec.shared_examples 'default allowlist' do expect(filter(act).to_html).to eq exp end - it 'sanitizes javascript in attributes' do - act = %q(Text) - exp = 'Text' - expect(filter(act).to_html).to eq exp - end - - it 'sanitizes mixed-cased javascript in attributes' do - act = %q(Text) - exp = 'Text' - expect(filter(act).to_html).to eq exp - end - it 'allows allowlisted HTML tags from the user' do exp = act = "
\n
Term
\n
Definition
\n
" expect(filter(act).to_html).to eq exp @@ -39,6 +27,13 @@ RSpec.shared_examples 'default allowlist' do act = %q(Emphasis) expect(filter(act).to_html).to eq %q(Emphasis) end + + it 'removes `rel` attribute from `a` elements' do + act = %q(Link) + exp = %q(Link) + + expect(filter(act).to_html).to eq exp + end end RSpec.shared_examples 'XSS prevention' do @@ -133,6 +128,18 @@ RSpec.shared_examples 'XSS prevention' do end end + it 'sanitizes javascript in attributes' do + act = %q(Text) + exp = 'Text' + expect(filter(act).to_html).to eq exp + end + + it 'sanitizes mixed-cased javascript in attributes' do + act = %q(Text) + exp = 'Text' + expect(filter(act).to_html).to eq exp + end + it 'disallows data links' do input = 'XSS' output = filter(input) @@ -149,13 +156,6 @@ RSpec.shared_examples 'XSS prevention' do end RSpec.shared_examples 'sanitize link' do - it 'removes `rel` attribute from `a` elements' do - act = %q(Link) - exp = %q(Link) - - expect(filter(act).to_html).to eq exp - end - it 'disallows invalid URIs' do expect(Addressable::URI).to receive(:parse).with('foo://example.com') .and_raise(Addressable::URI::InvalidURIError) @@ -180,3 +180,18 @@ RSpec.shared_examples 'sanitize link' do expect(act.to_html).to eq exp end end + +# not meant to be exhaustive, but verify that the pipeline is doing sanitization +RSpec.shared_examples 'sanitize pipeline' do + subject { described_class.to_html(act, project: nil) } + + it 'includes BaseSanitizationFilter' do + result = described_class.filters.filter { |filter| filter.ancestors.include? Banzai::Filter::BaseSanitizationFilter } + + expect(result).not_to be_empty + end + + it 'includes SanitizeLinkFilter' do + expect(described_class.filters).to include(Banzai::Filter::SanitizeLinkFilter) + end +end diff --git a/workhorse/go.mod b/workhorse/go.mod index 577e1f81b3f..ac0c5670c51 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -29,7 +29,7 @@ require ( gitlab.com/gitlab-org/labkit v1.21.0 go.uber.org/goleak v1.3.0 gocloud.dev v0.39.0 - golang.org/x/image v0.19.0 + golang.org/x/image v0.20.0 golang.org/x/net v0.28.0 golang.org/x/oauth2 v0.22.0 google.golang.org/grpc v1.65.0 @@ -122,7 +122,7 @@ require ( golang.org/x/mod v0.20.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect diff --git a/workhorse/go.sum b/workhorse/go.sum index ca2b12ee9ff..a23262896b2 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -554,8 +554,8 @@ golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/i golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= -golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -751,8 +751,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/yarn.lock b/yarn.lock index 471bef114f2..b4401ebf43e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1378,10 +1378,10 @@ vue-functional-data-merge "^3.1.0" vue-runtime-helpers "^1.1.2" -"@gitlab/web-ide@^0.0.1-dev-20240816130114": - version "0.0.1-dev-20240816130114" - resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240816130114.tgz#25a88d945095ea10bab9fbed5de1daea205b0bf1" - integrity sha512-Uv3n+l3oS5ywBWxzXhriFvxYUYw4KBHxlQJEIN3w0gzEiFgV7sYwQmJjCjhukN0PNCIX0akHZYwMm+ow/vD9IA== +"@gitlab/web-ide@^0.0.1-dev-20240909013227": + version "0.0.1-dev-20240909013227" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240909013227.tgz#6ba20cabe4b3dee8eacbb0e3aa4d71b49b30fecc" + integrity sha512-fWkkQ3Vm03NmDrJVmEO7nteRzXHj2J4GGfKifILpMeWjKp2X7nPjatHsbOWS8TqEVQUTrL5SB6yV+p6242fAtA== "@graphql-eslint/eslint-plugin@3.20.1": version "3.20.1" @@ -13104,16 +13104,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13166,7 +13157,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13180,13 +13171,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -14878,7 +14862,7 @@ worker-loader@^3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14896,15 +14880,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"