Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-10 12:08:24 +00:00
parent 8f9b584cb1
commit 81240d1817
127 changed files with 1766 additions and 1085 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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';

View File

@ -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 {

View File

@ -1,3 +0,0 @@
fragment AbuseReportNotePermissions on NotePermissions {
resolveNote
}

View File

@ -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

View File

@ -1,10 +1,5 @@
#import "./abuse_report_note.fragment.graphql"
mutation updateAbuseReportNote($input: UpdateNoteInput!) {
updateNote(input: $input) {
note {
...AbuseReportNote
}
errors
}
}

View File

@ -98,6 +98,7 @@ export default {
.post(`${this.link}.json`)
.then(() => {
this.isLoading = false;
this.isDisabled = false;
this.$emit('pipelineActionRequestComplete');
})

File diff suppressed because it is too large Load Diff

View File

@ -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;
};
}

View File

@ -47,7 +47,7 @@ export const useLegacyDiffs = defineStore('legacyDiffs', {
mrReviews: {},
latestDiff: true,
disableVirtualScroller: false,
pinnedFileHash: null,
linkedFileHash: null,
};
},
actions: {

View File

@ -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;
},
};

View File

@ -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"
],

View File

@ -0,0 +1,3 @@
import { defineStore } from 'pinia';
export const useMrNotes = defineStore('legacyMrNotes', {});

View File

@ -89,6 +89,7 @@ export default {
:value="commentLineStart"
:options="commentLineOptions"
width="sm"
class="gl-w-auto"
@change="updateCommentLineStart"
/>
</template>

View File

@ -0,0 +1,3 @@
import { defineStore } from 'pinia';
export const useNotes = defineStore('legacyNotes', {});

View File

@ -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;
}

View File

@ -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 {

View File

@ -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);
}
}
}
}
}

View File

@ -41,12 +41,6 @@
}
}
.toggle-colors {
input {
min-height: 34px;
}
}
.gl-broadcast-message-content p:last-child {
margin: 0;
}

View File

@ -20,10 +20,6 @@
color: $text;
border-color: $border;
&.btn-border-color {
border-color: $border-color;
}
> .icon {
color: $text;
}

View File

@ -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. 🚨

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Integrations
class InstanceIntegration < Integration
self.table_name = 'instance_integrations'
end
end

View File

@ -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.

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module AntiAbuse
module Reports
class NotePolicy < BasePolicy
delegate { @subject.abuse_report }
end
end
end

View File

@ -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)

View File

@ -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 } }

View File

@ -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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
- link_start_group = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_access_tokens_link }
= f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,

View File

@ -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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url }
%h5= _('Two-factor authentication')

View File

@ -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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/settings/group_access_tokens') }
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.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 } }

View File

@ -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

View File

@ -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')

View File

@ -10,7 +10,7 @@
%h4.gl-my-0
= page_title
%p.gl-text-secondary
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') }
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.html_safe }

View File

@ -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')

View File

@ -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 } }

View File

@ -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

View File

@ -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.
#

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
3a6da002969d32ed71e3a8700a007380d20193df5f3a3591e98136a135380f4f

View File

@ -0,0 +1 @@
7f15b5117aa826e8e0dbbd334393a29c4da7bc6443308f1ceffbaa015ab999cd

View File

@ -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);

View File

@ -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 |
| ---- | ---- | ----------- |
| <a id="abusereportdiscussionconnectionedges"></a>`edges` | [`[AbuseReportDiscussionEdge]`](#abusereportdiscussionedge) | A list of edges. |
| <a id="abusereportdiscussionconnectionnodes"></a>`nodes` | [`[AbuseReportDiscussion]`](#abusereportdiscussion) | A list of nodes. |
| <a id="abusereportdiscussionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `AbuseReportDiscussionEdge`
The edge type for [`AbuseReportDiscussion`](#abusereportdiscussion).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="abusereportdiscussionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="abusereportdiscussionedgenode"></a>`node` | [`AbuseReportDiscussion`](#abusereportdiscussion) | The item at the end of the edge. |
#### `AbuseReportNoteConnection`
The connection type for [`AbuseReportNote`](#abusereportnote).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="abusereportnoteconnectionedges"></a>`edges` | [`[AbuseReportNoteEdge]`](#abusereportnoteedge) | A list of edges. |
| <a id="abusereportnoteconnectionnodes"></a>`nodes` | [`[AbuseReportNote]`](#abusereportnote) | A list of nodes. |
| <a id="abusereportnoteconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `AbuseReportNoteEdge`
The edge type for [`AbuseReportNote`](#abusereportnote).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="abusereportnoteedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="abusereportnoteedgenode"></a>`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 |
| ---- | ---- | ----------- |
| <a id="abusereportcommenters"></a>`commenters` | [`UserCoreConnection!`](#usercoreconnection) | All commenters on this noteable. (see [Connections](#connections)) |
| <a id="abusereportdiscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
| <a id="abusereportdiscussions"></a>`discussions` | [`AbuseReportDiscussionConnection!`](#abusereportdiscussionconnection) | All discussions on the noteable. (see [Connections](#connections)) |
| <a id="abusereportid"></a>`id` | [`AbuseReportID!`](#abusereportid) | Global ID of the abuse report. |
| <a id="abusereportlabels"></a>`labels` | [`LabelConnection`](#labelconnection) | Labels of the abuse report. (see [Connections](#connections)) |
| <a id="abusereportnotes"></a>`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 |
| ---- | ---- | ----------- |
| <a id="abusereportnotesfilter"></a>`filter` | [`NotesFilterType`](#notesfiltertype) | Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY. |
| <a id="abusereportdiscussionabusereport"></a>`abuseReport` | [`AbuseReport`](#abusereport) | Abuse report which the discussion belongs to. |
| <a id="abusereportdiscussioncreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. |
| <a id="abusereportdiscussionid"></a>`id` | [`DiscussionID!`](#discussionid) | ID of the discussion. |
| <a id="abusereportdiscussionnotes"></a>`notes` | [`AbuseReportNoteConnection!`](#abusereportnoteconnection) | All notes in the discussion. (see [Connections](#connections)) |
| <a id="abusereportdiscussionreplyid"></a>`replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. |
| <a id="abusereportdiscussionresolvable"></a>`resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. |
| <a id="abusereportdiscussionresolved"></a>`resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. |
| <a id="abusereportdiscussionresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. |
| <a id="abusereportdiscussionresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. |
### `AbuseReportNote`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="abusereportnoteauthor"></a>`author` | [`UserCore`](#usercore) | User who wrote the note. |
| <a id="abusereportnoteawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of emoji reactions associated with the note. (see [Connections](#connections)) |
| <a id="abusereportnotebody"></a>`body` | [`String!`](#string) | Content of the note. |
| <a id="abusereportnotebodyfirstlinehtml"></a>`bodyFirstLineHtml` | [`String!`](#string) | First line of the note content. |
| <a id="abusereportnotebodyhtml"></a>`bodyHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of the content of the note. |
| <a id="abusereportnotecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the note creation. |
| <a id="abusereportnotediscussion"></a>`discussion` | [`AbuseReportDiscussion`](#abusereportdiscussion) | Discussion the note is a part of. |
| <a id="abusereportnoteid"></a>`id` | [`AntiAbuseReportsNoteID!`](#antiabusereportsnoteid) | ID of the note. |
| <a id="abusereportnotelasteditedat"></a>`lastEditedAt` | [`Time`](#time) | Timestamp when note was last edited. |
| <a id="abusereportnotelasteditedby"></a>`lastEditedBy` | [`UserCore`](#usercore) | User who last edited the note. |
| <a id="abusereportnoteresolvable"></a>`resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. |
| <a id="abusereportnoteresolved"></a>`resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. |
| <a id="abusereportnoteresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. |
| <a id="abusereportnoteresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. |
| <a id="abusereportnoteupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of the note's last activity. |
| <a id="abusereportnoteurl"></a>`url` | [`String`](#string) | URL to view the note in the Web UI. |
### `AccessLevel`
@ -17355,6 +17422,19 @@ Information about a connected Agent.
| <a id="aggregationstatusestimatednextupdateat"></a>`estimatedNextUpdateAt` | [`Time`](#time) | Estimated time when the next incremental update will happen. |
| <a id="aggregationstatuslastupdateat"></a>`lastUpdateAt` | [`Time`](#time) | Last incremental update time. |
### `AiAdditionalContext`
Additional context for AI message.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aiadditionalcontextcategory"></a>`category` | [`AiAdditionalContextCategory!`](#aiadditionalcontextcategory) | Category of the additional context. |
| <a id="aiadditionalcontextcontent"></a>`content` | [`String!`](#string) | Content of the additional context. |
| <a id="aiadditionalcontextid"></a>`id` | [`ID!`](#id) | ID of the additional context. |
| <a id="aiadditionalcontextmetadata"></a>`metadata` | [`JSON`](#json) | Metadata of the additional context. |
### `AiAgent`
An AI agent.
@ -17403,6 +17483,7 @@ AI features communication message.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="aimessageadditionalcontext"></a>`additionalContext` | [`[AiAdditionalContext!]`](#aiadditionalcontext) | Additional context for the message. |
| <a id="aimessageagentversionid"></a>`agentVersionId` | [`AiAgentVersionID`](#aiagentversionid) | Global ID of the agent version to answer the message. |
| <a id="aimessagechunkid"></a>`chunkId` | [`Int`](#int) | Incremental ID for a chunk from a streamed message. Null when it is not a streamed message. |
| <a id="aimessagecontent"></a>`content` | [`String`](#string) | Raw response content. |
@ -21570,10 +21651,10 @@ Aggregated summary of changes.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="discussioncreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. |
| <a id="discussionid"></a>`id` | [`DiscussionID!`](#discussionid) | ID of this discussion. |
| <a id="discussionid"></a>`id` | [`DiscussionID!`](#discussionid) | ID of the discussion. |
| <a id="discussionnoteable"></a>`noteable` | [`NoteableType`](#noteabletype) | Object which the discussion belongs to. |
| <a id="discussionnotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes in the discussion. (see [Connections](#connections)) |
| <a id="discussionreplyid"></a>`replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to this discussion. |
| <a id="discussionreplyid"></a>`replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. |
| <a id="discussionresolvable"></a>`resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. |
| <a id="discussionresolved"></a>`resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. |
| <a id="discussionresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. |
@ -35427,8 +35508,11 @@ LLMs supported by the self-hosted model features.
| <a id="aiacceptedselfhostedmodelscodestral"></a>`CODESTRAL` | Codestral 22B: Suitable for code completion and code generation. |
| <a id="aiacceptedselfhostedmodelsdeepseekcoder"></a>`DEEPSEEKCODER` | Deepseek Coder 1.3b, 6.7b and 33b base or instruct. |
| <a id="aiacceptedselfhostedmodelsmistral"></a>`MISTRAL` | Mistral 7B: Suitable for code generation and duo chat. |
| <a id="aiacceptedselfhostedmodelsmistral_text"></a>`MISTRAL_TEXT` | Mistral-7B Text: Suitable for code completion. |
| <a id="aiacceptedselfhostedmodelsmixtral"></a>`MIXTRAL` | Mixtral 8x7B: Suitable for code generation and duo chat. |
| <a id="aiacceptedselfhostedmodelsmixtral_8x22b"></a>`MIXTRAL_8X22B` | Mixtral 8x22B: Suitable for code generation and duo chat. |
| <a id="aiacceptedselfhostedmodelsmixtral_8x22b_text"></a>`MIXTRAL_8X22B_TEXT` | Mixtral-8x22B Text: Suitable for code completion. |
| <a id="aiacceptedselfhostedmodelsmixtral_text"></a>`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:
| <a id="auditeventstreamingdestinationinterfaceid"></a>`id` | [`ID!`](#id) | ID of the destination. |
| <a id="auditeventstreamingdestinationinterfacename"></a>`name` | [`String!`](#string) | Name of the external destination to send audit events to. |
#### `BaseDiscussionInterface`
Implementations:
- [`AbuseReportDiscussion`](#abusereportdiscussion)
- [`Discussion`](#discussion)
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="basediscussioninterfacecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the discussion's creation. |
| <a id="basediscussioninterfaceid"></a>`id` | [`DiscussionID!`](#discussionid) | ID of the discussion. |
| <a id="basediscussioninterfacereplyid"></a>`replyId` | [`DiscussionID!`](#discussionid) | ID used to reply to the discussion. |
| <a id="basediscussioninterfaceresolvable"></a>`resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. |
| <a id="basediscussioninterfaceresolved"></a>`resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. |
| <a id="basediscussioninterfaceresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. |
| <a id="basediscussioninterfaceresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. |
#### `BaseHeaderInterface`
Implementations:
@ -40017,6 +40126,32 @@ Implementations:
| <a id="baseheaderinterfacekey"></a>`key` | [`String!`](#string) | Key of the header. |
| <a id="baseheaderinterfacevalue"></a>`value` | [`String!`](#string) | Value of the header. |
#### `BaseNoteInterface`
Implementations:
- [`AbuseReportNote`](#abusereportnote)
- [`Note`](#note)
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="basenoteinterfaceauthor"></a>`author` | [`UserCore`](#usercore) | User who wrote the note. |
| <a id="basenoteinterfaceawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of emoji reactions associated with the note. (see [Connections](#connections)) |
| <a id="basenoteinterfacebody"></a>`body` | [`String!`](#string) | Content of the note. |
| <a id="basenoteinterfacebodyfirstlinehtml"></a>`bodyFirstLineHtml` | [`String!`](#string) | First line of the note content. |
| <a id="basenoteinterfacebodyhtml"></a>`bodyHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of the content of the note. |
| <a id="basenoteinterfacecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the note creation. |
| <a id="basenoteinterfacelasteditedat"></a>`lastEditedAt` | [`Time`](#time) | Timestamp when note was last edited. |
| <a id="basenoteinterfacelasteditedby"></a>`lastEditedBy` | [`UserCore`](#usercore) | User who last edited the note. |
| <a id="basenoteinterfaceresolvable"></a>`resolvable` | [`Boolean!`](#boolean) | Indicates if the object can be resolved. |
| <a id="basenoteinterfaceresolved"></a>`resolved` | [`Boolean!`](#boolean) | Indicates if the object is resolved. |
| <a id="basenoteinterfaceresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the object was resolved. |
| <a id="basenoteinterfaceresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | User who resolved the object. |
| <a id="basenoteinterfaceupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of the note's last activity. |
| <a id="basenoteinterfaceurl"></a>`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)

View File

@ -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

View File

@ -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.

View File

@ -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).

View File

@ -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 |

View File

@ -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**.

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -10,8 +10,6 @@ module Banzai
prepend Concerns::TimeoutFilterHandler
prepend Concerns::PipelineTimingCheck
RENDER_TIMEOUT = 2.seconds
def initialize(doc, context = nil, result = nil)
super

View File

@ -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
<p>Timeout while sanitizing links - rendering aborted. Please reduce the number of links if possible.</p>
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

View File

@ -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

View File

@ -6,6 +6,7 @@ module Banzai
def self.filters
FilterArray[
Filter::AsciiDocSanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::CodeLanguageFilter,
Filter::GollumTagsFilter,
Filter::WikiLinkGollumFilter,

View File

@ -8,6 +8,7 @@ module Banzai
Filter::BlockquoteFenceLegacyFilter,
Filter::MarkdownFilter,
Filter::BroadcastMessageSanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::EmojiFilter,
Filter::ColorFilter,
Filter::AutolinkFilter,

View File

@ -9,6 +9,7 @@ module Banzai
@filters ||= FilterArray[
Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::EmojiFilter
]
end

View File

@ -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,

View File

@ -12,6 +12,7 @@ module Banzai
@filters ||= FilterArray[
*super,
Filter::SanitizationFilter,
Filter::SanitizeLinkFilter,
*Banzai::Pipeline::GfmPipeline.reference_filters,
Filter::EmojiFilter,
Filter::ExternalLinkFilter,

View File

@ -6,6 +6,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::References::LabelReferenceFilter
]
end

View File

@ -6,6 +6,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::CodeLanguageFilter,
Filter::AssetProxyFilter,
Filter::ExternalLinkFilter,

View File

@ -7,6 +7,7 @@ module Banzai
@filters ||= FilterArray[
Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::SanitizeLinkFilter,
Filter::AssetProxyFilter,
Filter::EmojiFilter,
Filter::CustomEmojiFilter,

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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",

View File

@ -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 () {

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :instance_integration, class: 'Integrations::InstanceIntegration' do
type { 'Integrations::InstanceIntegration' }
end
end

View File

@ -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

View File

@ -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,
});

View File

@ -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',
},

View File

@ -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);
});
});

View File

@ -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',
]);
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

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