Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8986fe3483
commit
60028378dd
2
Gemfile
2
Gemfile
|
|
@ -476,7 +476,7 @@ gem 'ssh_data', '~> 1.2'
|
|||
gem 'spamcheck', '~> 0.1.0'
|
||||
|
||||
# Gitaly GRPC protocol definitions
|
||||
gem 'gitaly', '~> 14.8.0.pre.rc1'
|
||||
gem 'gitaly', '~> 14.9.0.pre.rc2'
|
||||
|
||||
# KAS GRPC protocol definitions
|
||||
gem 'kas-grpc', '~> 0.0.2'
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ GEM
|
|||
rails (>= 3.2.0)
|
||||
git (1.7.0)
|
||||
rchardet (~> 1.8)
|
||||
gitaly (14.8.0.pre.rc1)
|
||||
gitaly (14.9.0.pre.rc2)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab (4.16.1)
|
||||
|
|
@ -1475,7 +1475,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.3)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly (~> 14.8.0.pre.rc1)
|
||||
gitaly (~> 14.9.0.pre.rc2)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-dangerfiles (~> 2.10.2)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { sortBy, cloneDeep } from 'lodash';
|
||||
import { isGid } from '~/graphql_shared/utils';
|
||||
import { TYPE_BOARD, TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
|
||||
import { isGid, convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { ListType, MilestoneIDs, AssigneeFilterType, MilestoneFilterType } from './constants';
|
||||
|
||||
export function getMilestone() {
|
||||
|
|
@ -80,19 +81,22 @@ export function formatListsPageInfo(lists) {
|
|||
}
|
||||
|
||||
export function fullBoardId(boardId) {
|
||||
return `gid://gitlab/Board/${boardId}`;
|
||||
if (!boardId) {
|
||||
return null;
|
||||
}
|
||||
return convertToGraphQLId(TYPE_BOARD, boardId);
|
||||
}
|
||||
|
||||
export function fullIterationId(id) {
|
||||
return `gid://gitlab/Iteration/${id}`;
|
||||
return convertToGraphQLId(TYPE_ITERATION, id);
|
||||
}
|
||||
|
||||
export function fullUserId(id) {
|
||||
return `gid://gitlab/User/${id}`;
|
||||
return convertToGraphQLId(TYPE_USER, id);
|
||||
}
|
||||
|
||||
export function fullMilestoneId(id) {
|
||||
return `gid://gitlab/Milestone/${id}`;
|
||||
return convertToGraphQLId(TYPE_MILESTONE, id);
|
||||
}
|
||||
|
||||
export function fullLabelId(label) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
<script>
|
||||
import { GlModal, GlAlert } from '@gitlab/ui';
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { fullLabelId } from '../boards_util';
|
||||
import { formType } from '../constants';
|
||||
|
||||
import createBoardMutation from '../graphql/board_create.mutation.graphql';
|
||||
|
|
@ -158,33 +155,8 @@ export default {
|
|||
groupPath: this.isGroupBoard ? this.fullPath : undefined,
|
||||
};
|
||||
},
|
||||
issueBoardScopeMutationVariables() {
|
||||
return {
|
||||
weight: this.board.weight,
|
||||
assigneeId: this.board.assignee?.id
|
||||
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
|
||||
: null,
|
||||
// Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
|
||||
milestoneId: this.board.milestone?.id
|
||||
? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id))
|
||||
: null,
|
||||
// Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779
|
||||
iterationId: this.board.iteration?.id
|
||||
? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id))
|
||||
: null,
|
||||
};
|
||||
},
|
||||
boardScopeMutationVariables() {
|
||||
return {
|
||||
labelIds: this.board.labels.map(fullLabelId),
|
||||
...(this.isIssueBoard && this.issueBoardScopeMutationVariables),
|
||||
};
|
||||
},
|
||||
mutationVariables() {
|
||||
return {
|
||||
...this.baseMutationVariables,
|
||||
...(this.scopedIssueBoardFeatureEnabled ? this.boardScopeMutationVariables : {}),
|
||||
};
|
||||
return this.baseMutationVariables;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export default {
|
|||
},
|
||||
update(data) {
|
||||
const board = data.workspace?.board;
|
||||
this.setBoardConfig(board);
|
||||
return {
|
||||
...board,
|
||||
labels: board?.labels?.nodes,
|
||||
|
|
@ -170,7 +171,7 @@ export default {
|
|||
eventHub.$off('showBoardModal', this.showPage);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setError']),
|
||||
...mapActions(['setError', 'setBoardConfig']),
|
||||
showPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ const apolloProvider = new VueApollo({
|
|||
function mountBoardApp(el) {
|
||||
const { boardId, groupId, fullPath, rootPath } = el.dataset;
|
||||
|
||||
store.dispatch('fetchBoard', {
|
||||
fullPath,
|
||||
fullBoardId: fullBoardId(boardId),
|
||||
boardType: el.dataset.parent,
|
||||
});
|
||||
|
||||
store.dispatch('setInitialBoardData', {
|
||||
boardId,
|
||||
fullBoardId: fullBoardId(boardId),
|
||||
|
|
@ -35,17 +41,6 @@ function mountBoardApp(el) {
|
|||
boardType: el.dataset.parent,
|
||||
disabled: parseBoolean(el.dataset.disabled) || true,
|
||||
issuableType: issuableTypes.issue,
|
||||
boardConfig: {
|
||||
milestoneId: parseInt(el.dataset.boardMilestoneId, 10),
|
||||
milestoneTitle: el.dataset.boardMilestoneTitle || '',
|
||||
iterationId: parseInt(el.dataset.boardIterationId, 10),
|
||||
iterationTitle: el.dataset.boardIterationTitle || '',
|
||||
assigneeId: el.dataset.boardAssigneeId,
|
||||
assigneeUsername: el.dataset.boardAssigneeUsername,
|
||||
labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [],
|
||||
labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [],
|
||||
weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
|||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import { gqlClient } from '../graphql';
|
||||
import projectBoardQuery from '../graphql/project_board.query.graphql';
|
||||
import groupBoardQuery from '../graphql/group_board.query.graphql';
|
||||
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
|
||||
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
|
||||
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
|
||||
|
|
@ -46,10 +48,44 @@ import projectBoardMilestonesQuery from '../graphql/project_board_milestones.que
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => {
|
||||
const variables = {
|
||||
fullPath,
|
||||
boardId: fullBoardId,
|
||||
};
|
||||
|
||||
return gqlClient
|
||||
.query({
|
||||
query: boardType === BoardType.group ? groupBoardQuery : projectBoardQuery,
|
||||
variables,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const board = data.workspace?.board;
|
||||
commit(types.RECEIVE_BOARD_SUCCESS, board);
|
||||
dispatch('setBoardConfig', board);
|
||||
})
|
||||
.catch(() => commit(types.RECEIVE_BOARD_FAILURE));
|
||||
},
|
||||
|
||||
setInitialBoardData: ({ commit }, data) => {
|
||||
commit(types.SET_INITIAL_BOARD_DATA, data);
|
||||
},
|
||||
|
||||
setBoardConfig: ({ commit }, board) => {
|
||||
const config = {
|
||||
milestoneId: board.milestone?.id || null,
|
||||
milestoneTitle: board.milestone?.title || null,
|
||||
iterationId: board.iteration?.id || null,
|
||||
iterationTitle: board.iteration?.title || null,
|
||||
assigneeId: board.assignee?.id || null,
|
||||
assigneeUsername: board.assignee?.username || null,
|
||||
labels: board.labels?.nodes || [],
|
||||
labelIds: board.labels?.nodes?.map((label) => label.id) || [],
|
||||
weight: board.weight,
|
||||
};
|
||||
commit(types.SET_BOARD_CONFIG, config);
|
||||
},
|
||||
|
||||
setActiveId({ commit }, { id, sidebarType }) {
|
||||
commit(types.SET_ACTIVE_ID, { id, sidebarType });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS';
|
||||
export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE';
|
||||
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
|
||||
export const SET_BOARD_CONFIG = 'SET_BOARD_CONFIG';
|
||||
export const SET_FILTERS = 'SET_FILTERS';
|
||||
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
|
||||
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
|
||||
|
|
|
|||
|
|
@ -33,10 +33,20 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
|
|||
};
|
||||
|
||||
export default {
|
||||
[mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => {
|
||||
state.board = {
|
||||
...board,
|
||||
labels: board?.labels?.nodes || [],
|
||||
};
|
||||
},
|
||||
|
||||
[mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => {
|
||||
state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.');
|
||||
},
|
||||
|
||||
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
|
||||
const {
|
||||
allowSubEpics,
|
||||
boardConfig,
|
||||
boardId,
|
||||
boardType,
|
||||
disabled,
|
||||
|
|
@ -45,7 +55,6 @@ export default {
|
|||
issuableType,
|
||||
} = data;
|
||||
state.allowSubEpics = allowSubEpics;
|
||||
state.boardConfig = boardConfig;
|
||||
state.boardId = boardId;
|
||||
state.boardType = boardType;
|
||||
state.disabled = disabled;
|
||||
|
|
@ -54,6 +63,10 @@ export default {
|
|||
state.issuableType = issuableType;
|
||||
},
|
||||
|
||||
[mutationTypes.SET_BOARD_CONFIG](state, boardConfig) {
|
||||
state.boardConfig = boardConfig;
|
||||
},
|
||||
|
||||
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
|
||||
state.boardLists = lists;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { inactiveId, ListType } from '~/boards/constants';
|
||||
|
||||
export default () => ({
|
||||
board: {},
|
||||
boardType: null,
|
||||
issuableType: null,
|
||||
fullPath: null,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import '~/lib/utils/jquery_at_who';
|
|||
import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash';
|
||||
import * as Emoji from '~/emoji';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { isUserBusy } from '~/set_status_modal/utils';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
|
|
@ -957,9 +958,14 @@ GfmAutoComplete.Contacts = {
|
|||
return `<li><small>${firstName} ${lastName}</small> ${escape(email)}</li>`;
|
||||
},
|
||||
};
|
||||
|
||||
const loadingSpinner = loadingIconForLegacyJS({
|
||||
inline: true,
|
||||
classes: ['gl-mr-2'],
|
||||
}).outerHTML;
|
||||
|
||||
GfmAutoComplete.Loading = {
|
||||
template:
|
||||
'<li style="pointer-events: none;"><span class="spinner align-text-bottom mr-1"></span>Loading...</li>',
|
||||
template: `<li style="pointer-events: none;">${loadingSpinner}Loading...</li>`,
|
||||
};
|
||||
|
||||
export default GfmAutoComplete;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
export const MINIMUM_SEARCH_LENGTH = 3;
|
||||
|
||||
export const TYPE_BOARD = 'Board';
|
||||
export const TYPE_CI_RUNNER = 'Ci::Runner';
|
||||
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
|
||||
export const TYPE_DISCUSSION = 'Discussion';
|
||||
export const TYPE_EPIC = 'Epic';
|
||||
export const TYPE_EPIC_BOARD = 'Boards::EpicBoard';
|
||||
export const TYPE_GROUP = 'Group';
|
||||
export const TYPE_ISSUE = 'Issue';
|
||||
export const TYPE_ITERATION = 'Iteration';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
|
|||
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
|
||||
// OR a number with a . after it and an optional checkbox ([ ] [x])
|
||||
// followed by one or more whitespace characters
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
|
||||
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
|
||||
|
||||
function selectedText(text, textarea) {
|
||||
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
|
|
@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) {
|
|||
return split[split.length - 1];
|
||||
}
|
||||
|
||||
function lineAfter(text, textarea) {
|
||||
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
|
||||
function lineAfter(text, textarea, trimNewlines = true) {
|
||||
let split = text.substring(textarea.selectionEnd);
|
||||
|
||||
if (trimNewlines) {
|
||||
split = split.trim();
|
||||
} else {
|
||||
// remove possible leading newline to get at the real line
|
||||
split = split.replace(/^\n/, '');
|
||||
}
|
||||
|
||||
split = split.split('\n');
|
||||
|
||||
return split[0];
|
||||
}
|
||||
|
||||
function convertMonacoSelectionToAceFormat(sel) {
|
||||
|
|
@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) {
|
|||
}
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
/**
|
||||
* Returns the content for a new line following a list item.
|
||||
*
|
||||
* @param {Object} result - regex match of the current line
|
||||
* @param {Object?} nextLineResult - regex match of the next line
|
||||
* @returns string with the new list item
|
||||
*/
|
||||
function continueOlText(result, nextLineResult) {
|
||||
const { indent, leader } = result.groups;
|
||||
const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {};
|
||||
|
||||
const [numStr, postfix = ''] = leader.split('.');
|
||||
|
||||
const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1;
|
||||
const num = parseInt(numStr, 10) + incrementBy;
|
||||
|
||||
return `${indent}${num}.${postfix}`;
|
||||
}
|
||||
|
||||
function handleContinueList(e, textArea) {
|
||||
if (!gon.features?.markdownContinueLists) return;
|
||||
if (!(e.key === 'Enter')) return;
|
||||
|
|
@ -339,7 +369,7 @@ function handleContinueList(e, textArea) {
|
|||
const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
|
||||
|
||||
if (result) {
|
||||
const { indent, content, leader } = result.groups;
|
||||
const { leader, indent, content, isOl } = result.groups;
|
||||
const prevLineEmpty = !content;
|
||||
|
||||
if (prevLineEmpty) {
|
||||
|
|
@ -349,12 +379,22 @@ function handleContinueList(e, textArea) {
|
|||
return;
|
||||
}
|
||||
|
||||
const itemInsert = `${indent}${leader}`;
|
||||
let itemToInsert;
|
||||
|
||||
if (isOl) {
|
||||
const nextLine = lineAfter(textArea.value, textArea, false);
|
||||
const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN);
|
||||
|
||||
itemToInsert = continueOlText(result, nextLineResult);
|
||||
} else {
|
||||
// isUl
|
||||
itemToInsert = `${indent}${leader}`;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
updateText({
|
||||
tag: itemInsert,
|
||||
tag: itemToInsert,
|
||||
textArea,
|
||||
blockTag: '',
|
||||
wrap: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable consistent-return */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
|
||||
import { spriteIcon } from '~/lib/utils/common_utils';
|
||||
import FilesCommentButton from './files_comment_button';
|
||||
import createFlash from './flash';
|
||||
|
|
@ -10,7 +11,7 @@ import { __ } from './locale';
|
|||
import syntaxHighlight from './syntax_highlight';
|
||||
|
||||
const WRAPPER = '<div class="diff-content"></div>';
|
||||
const LOADING_HTML = '<span class="spinner"></span>';
|
||||
const LOADING_HTML = loadingIconForLegacyJS().outerHTML;
|
||||
const ERROR_HTML = `<div class="nothing-here-block">${spriteIcon(
|
||||
'warning-solid',
|
||||
's16',
|
||||
|
|
|
|||
|
|
@ -35,16 +35,20 @@ export default {
|
|||
},
|
||||
highlightedContent() {
|
||||
let highlightedContent;
|
||||
let { language } = this;
|
||||
|
||||
if (this.hljs) {
|
||||
if (!this.language) {
|
||||
highlightedContent = this.hljs.highlightAuto(this.content).value;
|
||||
if (!language) {
|
||||
const hljsHighlightAuto = this.hljs.highlightAuto(this.content);
|
||||
|
||||
highlightedContent = hljsHighlightAuto.value;
|
||||
language = hljsHighlightAuto.language;
|
||||
} else if (this.languageDefinition) {
|
||||
highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value;
|
||||
}
|
||||
}
|
||||
|
||||
return wrapLines(highlightedContent);
|
||||
return wrapLines(highlightedContent, language);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -110,7 +114,7 @@ export default {
|
|||
data-qa-selector="blob_viewer_file_content"
|
||||
>
|
||||
<line-numbers :lines="lineNumbers" />
|
||||
<pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code>
|
||||
<pre class="code highlight gl-pb-0!"><code v-safe-html="highlightedContent"></code>
|
||||
</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
export const wrapLines = (content) => {
|
||||
export const wrapLines = (content, language) => {
|
||||
const isValidLanguage = /^[a-z\d\-_]+$/.test(language); // To prevent the possibility of a vulnerability we only allow languages that contain alphanumeric characters ([a-z\d), dashes (-) or underscores (_).
|
||||
|
||||
return (
|
||||
content &&
|
||||
content
|
||||
.split('\n')
|
||||
.map((line, i) => {
|
||||
let formattedLine;
|
||||
const idAttribute = `id="LC${i + 1}"`;
|
||||
const attributes = `id="LC${i + 1}" lang="${isValidLanguage ? language : ''}"`;
|
||||
|
||||
if (line.includes('<span class="hljs') && !line.includes('</span>')) {
|
||||
/**
|
||||
|
|
@ -14,9 +16,9 @@ export const wrapLines = (content) => {
|
|||
* example (before): <span class="hljs-code">```bash
|
||||
* example (after): <span id="LC67" class="hljs-code">```bash
|
||||
*/
|
||||
formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `);
|
||||
formattedLine = line.replace(/(?=class="hljs)/, `${attributes} `);
|
||||
} else {
|
||||
formattedLine = `<span ${idAttribute} class="line">${line}</span>`;
|
||||
formattedLine = `<span ${attributes} class="line">${line}</span>`;
|
||||
}
|
||||
|
||||
return formattedLine;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ class ApplicationRecord < ActiveRecord::Base
|
|||
include Transactions
|
||||
include LegacyBulkInsert
|
||||
include CrossDatabaseModification
|
||||
include SensitiveSerializableHash
|
||||
|
||||
self.abstract_class = true
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SensitiveSerializableHash
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :attributes_exempt_from_serializable_hash, default: []
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def prevent_from_serialization(*keys)
|
||||
self.attributes_exempt_from_serializable_hash ||= []
|
||||
self.attributes_exempt_from_serializable_hash.concat keys
|
||||
end
|
||||
end
|
||||
|
||||
# Override serializable_hash to exclude sensitive attributes by default
|
||||
#
|
||||
# In general, prefer NOT to use serializable_hash / to_json / as_json in favor
|
||||
# of serializers / entities instead which has an allowlist of attributes
|
||||
def serializable_hash(options = nil)
|
||||
return super unless prevent_sensitive_fields_from_serializable_hash?
|
||||
return super if options && options[:unsafe_serialization_hash]
|
||||
|
||||
options = options.try(:dup) || {}
|
||||
options[:except] = Array(options[:except]).dup
|
||||
|
||||
options[:except].concat self.class.attributes_exempt_from_serializable_hash
|
||||
|
||||
if self.class.respond_to?(:encrypted_attributes)
|
||||
options[:except].concat self.class.encrypted_attributes.keys
|
||||
|
||||
# Per https://github.com/attr-encrypted/attr_encrypted/blob/a96693e9a2a25f4f910bf915e29b0f364f277032/lib/attr_encrypted.rb#L413
|
||||
options[:except].concat self.class.encrypted_attributes.values.map { |v| v[:attribute] }
|
||||
options[:except].concat self.class.encrypted_attributes.values.map { |v| "#{v[:attribute]}_iv" }
|
||||
end
|
||||
|
||||
super(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prevent_sensitive_fields_from_serializable_hash?
|
||||
Feature.enabled?(:prevent_sensitive_fields_from_serializable_hash, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,10 @@ module TokenAuthenticatable
|
|||
@encrypted_token_authenticatable_fields ||= []
|
||||
end
|
||||
|
||||
def token_authenticatable_fields
|
||||
@token_authenticatable_fields ||= []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_authentication_token_field(token_field, options = {})
|
||||
|
|
@ -23,6 +27,8 @@ module TokenAuthenticatable
|
|||
strategy = TokenAuthenticatableStrategies::Base
|
||||
.fabricate(self, token_field, options)
|
||||
|
||||
prevent_from_serialization(*strategy.token_fields) if respond_to?(:prevent_from_serialization)
|
||||
|
||||
if options.fetch(:unique, true)
|
||||
define_singleton_method("find_by_#{token_field}") do |token|
|
||||
strategy.find_token_authenticatable(token)
|
||||
|
|
@ -82,9 +88,5 @@ module TokenAuthenticatable
|
|||
@token_authenticatable_module ||=
|
||||
const_set(:TokenAuthenticatable, Module.new).tap(&method(:include))
|
||||
end
|
||||
|
||||
def token_authenticatable_fields
|
||||
@token_authenticatable_fields ||= []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,14 @@ module TokenAuthenticatableStrategies
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def token_fields
|
||||
result = [token_field]
|
||||
|
||||
result << @expires_at_field if expirable?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Default implementation returns the token as-is
|
||||
def format_token(instance, token)
|
||||
instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
module TokenAuthenticatableStrategies
|
||||
class Digest < Base
|
||||
def token_fields
|
||||
super + [token_field_name]
|
||||
end
|
||||
|
||||
def find_token_authenticatable(token, unscoped = false)
|
||||
return unless token
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
module TokenAuthenticatableStrategies
|
||||
class Encrypted < Base
|
||||
def token_fields
|
||||
super + [encrypted_field]
|
||||
end
|
||||
|
||||
def find_token_authenticatable(token, unscoped = false)
|
||||
return if token.blank?
|
||||
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ class Wiki
|
|||
# the page.
|
||||
#
|
||||
# Returns an initialized WikiPage instance or nil
|
||||
def find_page(title, version = nil)
|
||||
def find_page(title, version = nil, load_content: true)
|
||||
page_title, page_dir = page_title_and_dir(title)
|
||||
|
||||
if page = wiki.page(title: page_title, version: version, dir: page_dir)
|
||||
if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
|
||||
WikiPage.new(self, page)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: prevent_sensitive_fields_from_serializable_hash
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81773
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353878
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::sharding
|
||||
default_enabled: false
|
||||
|
|
@ -5,7 +5,7 @@ def feature_mr?
|
|||
end
|
||||
|
||||
DOCUMENTATION_UPDATE_MISSING = <<~MSG
|
||||
~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers).
|
||||
~"feature::addition" and ~"feature::enhancement" merge requests normally have a documentation change. Consider adding a documentation update or confirming the documentation plan with the [Technical Writer counterpart](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
|
||||
|
||||
For more information, see:
|
||||
|
||||
|
|
@ -36,6 +36,6 @@ markdown(<<~MARKDOWN)
|
|||
The review does not need to block merging this merge request. See the:
|
||||
|
||||
- [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change.
|
||||
- The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for that stage and group.
|
||||
- The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#assignments) for that stage and group.
|
||||
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
|
||||
MARKDOWN
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
|
||||
type: reference
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Package information **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Package Licensing **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Omnibus based packages and images **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# PostgreSQL versions shipped with Omnibus GitLab **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Package Signatures **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Ecosystem
|
||||
group: Integrations
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
<!---
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
type: reference, howto
|
||||
stage: Manage
|
||||
group: Authentication and Authorization
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# OAuth 2.0 identity provider API **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Package
|
||||
group: Package
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
comments: false
|
||||
description: 'Container Registry metadata database'
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
comments: false
|
||||
description: 'GitLab to Kubernetes communication'
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Verify
|
||||
group: Pipeline Authoring
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
type: reference
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# GitLab Group Migration
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Check these aspects both when _designing_ and _reviewing_ UI changes.
|
|||
- Use clear and consistent [terminology](https://design.gitlab.com/content/terminology/).
|
||||
- Check grammar and spelling.
|
||||
- Consider help content and follow its [guidelines](https://design.gitlab.com/usability/helping-users/).
|
||||
- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers),
|
||||
- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments),
|
||||
indicating any specific files or lines they should review, and how to preview
|
||||
or understand the location/context of the text from the user's perspective.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Export to CSV
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ persistence and is used to store session data, temporary cache information, and
|
|||
chance to deploy Redis in multiple availability zones.
|
||||
1. In the settings section:
|
||||
1. Give the cluster a name (`gitlab-redis`) and a description.
|
||||
1. For the version, select the latest of the `5.0` series (for example, `5.0.6`).
|
||||
1. For the version, select the latest.
|
||||
1. Leave the port as `6379` since this is what we used in our Redis security group above.
|
||||
1. Select the node type (at least `cache.t3.medium`, but adjust to your needs) and the number of replicas.
|
||||
1. In the advanced settings section:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Secure
|
||||
group: Static Analysis
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
type: index
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Convert Community Edition to Enterprise Edition **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Downgrade **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Upgrade GitLab by using the GitLab package **(FREE SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Enablement
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Spamcheck anti-spam service **(PREMIUM SELF)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Secure
|
||||
group: Dynamic Analysis
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
type: howto
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Using a GitOps workflow for Kubernetes **(PREMIUM)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Managing Kubernetes secrets in a GitOps workflow
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Working with the agent for Kubernetes **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Container vulnerability scanning **(ULTIMATE)**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
type: reference
|
||||
stage: Manage
|
||||
group: Optimize
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Value stream analytics for groups **(PREMIUM)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Protect
|
||||
group: Container Security
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Container Host Security **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Protect
|
||||
group: Container Security
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Getting started with Container Host Security **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Protect
|
||||
group: Container Security
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Container Network Security **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Protect
|
||||
group: Container Security
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Getting started with Container Network Security **(FREE)**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
stage: Protect
|
||||
group: Container Security
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Protecting your deployed applications **(FREE)**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
type: howto
|
||||
stage: Fulfillment
|
||||
group: Utilization
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Storage usage quota **(FREE)**
|
||||
|
|
|
|||
|
|
@ -93,9 +93,9 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def page(title:, version: nil, dir: nil)
|
||||
def page(title:, version: nil, dir: nil, load_content: true)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_find_page(title: title, version: version, dir: dir)
|
||||
gitaly_find_page(title: title, version: version, dir: dir, load_content: load_content)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -121,10 +121,10 @@ module Gitlab
|
|||
gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
|
||||
end
|
||||
|
||||
def gitaly_find_page(title:, version: nil, dir: nil)
|
||||
def gitaly_find_page(title:, version: nil, dir: nil, load_content: true)
|
||||
return unless title.present?
|
||||
|
||||
wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
|
||||
wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir, load_content: load_content)
|
||||
return unless wiki_page
|
||||
|
||||
Gitlab::Git::WikiPage.new(wiki_page, version)
|
||||
|
|
|
|||
|
|
@ -64,12 +64,13 @@ module Gitlab
|
|||
GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum, timeout: GitalyClient.medium_timeout)
|
||||
end
|
||||
|
||||
def find_page(title:, version: nil, dir: nil)
|
||||
def find_page(title:, version: nil, dir: nil, load_content: true)
|
||||
request = Gitaly::WikiFindPageRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
title: encode_binary(title),
|
||||
revision: encode_binary(version),
|
||||
directory: encode_binary(dir)
|
||||
directory: encode_binary(dir),
|
||||
skip_content: !load_content
|
||||
)
|
||||
|
||||
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request, timeout: GitalyClient.fast_timeout)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def write(key, value, options = nil)
|
||||
backend.write(cache_key(key), value.to_json, options)
|
||||
# As we use json as the serialization format, return everything from
|
||||
# ActiveModel objects included encrypted values.
|
||||
backend.write(cache_key(key), value.to_json(unsafe_serialization_hash: true), options)
|
||||
end
|
||||
|
||||
def fetch(key, options = {}, &block)
|
||||
|
|
|
|||
|
|
@ -5885,6 +5885,9 @@ msgstr ""
|
|||
msgid "Boards|An error occurred while fetching the board swimlanes. Please reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "Boards|An error occurred while fetching the board. Please reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "Boards|An error occurred while generating lists. Please reload the page."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'gitlab_project_migration_common'
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage' do
|
||||
describe 'Gitlab migration' do
|
||||
include_context 'with gitlab project migration'
|
||||
|
||||
context 'with ci pipeline' do
|
||||
let!(:source_project_with_readme) { true }
|
||||
|
||||
let(:source_pipelines) do
|
||||
source_project.pipelines.map do |pipeline|
|
||||
pipeline.except(:id, :web_url, :project_id)
|
||||
end
|
||||
end
|
||||
|
||||
let(:imported_pipelines) do
|
||||
imported_project.pipelines.map do |pipeline|
|
||||
pipeline.except(:id, :web_url, :project_id)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.api_client = api_client
|
||||
commit.project = source_project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YML
|
||||
test-success:
|
||||
script: echo 'OK'
|
||||
YML
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
Support::Waiter.wait_until(max_duration: 10, sleep_interval: 1) do
|
||||
!source_project.pipelines.empty?
|
||||
end
|
||||
end
|
||||
|
||||
it(
|
||||
'successfully imports ci pipeline',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354650'
|
||||
) do
|
||||
expect_import_finished
|
||||
|
||||
expect(imported_pipelines).to eq(source_pipelines)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
# Disable on staging until bulk_import_projects toggle is on by default
|
||||
# Disable on live envs until bulk_import_projects toggle is on by default
|
||||
# Otherwise tests running in parallel can disable feature in the middle of other test
|
||||
RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging } do
|
||||
RSpec.shared_context 'with gitlab project migration', :requires_admin, :skip_live_env do
|
||||
let(:source_project_with_readme) { false }
|
||||
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
|
||||
let(:admin_api_client) { Runtime::API::Client.as_admin }
|
||||
|
|
@ -79,8 +79,6 @@ module QA
|
|||
# Log failures for easier debugging
|
||||
Runtime::Logger.warn("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
|
||||
ensure
|
||||
Runtime::Feature.disable(:bulk_import_projects)
|
||||
|
||||
user.remove_via_api!
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ RSpec.describe 'Copy as GFM', :js do
|
|||
include RepoHelpers
|
||||
include ActionView::Helpers::JavaScriptHelper
|
||||
|
||||
before do
|
||||
stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350454
|
||||
end
|
||||
|
||||
describe 'Copying rendered GFM' do
|
||||
before do
|
||||
@feat = MarkdownFeature.new
|
||||
|
|
@ -764,8 +760,8 @@ RSpec.describe 'Copy as GFM', :js do
|
|||
context 'selecting one word of text' do
|
||||
it 'copies as inline code' do
|
||||
verify(
|
||||
'.line[id="LC9"] .no',
|
||||
'`RuntimeError`'
|
||||
'.line[id="LC10"]',
|
||||
'`end`'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -834,6 +830,7 @@ RSpec.describe 'Copy as GFM', :js do
|
|||
end
|
||||
|
||||
def verify(selector, gfm, target: nil)
|
||||
expect(page).to have_selector('.js-syntax-highlight')
|
||||
html = html_for_selector(selector)
|
||||
output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
|
||||
wait_for_requests
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe 'User triggers manual job with variables', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'key_name', 'value' => 'key_value'))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe('BoardsSelector', () => {
|
|||
...defaultStore,
|
||||
actions: {
|
||||
setError: jest.fn(),
|
||||
setBoardConfig: jest.fn(),
|
||||
},
|
||||
getters: {
|
||||
isGroupBoard: () => isGroupBoard,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,37 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
|
|||
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
|
||||
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
|
||||
|
||||
export const mockBoard = {
|
||||
milestone: {
|
||||
id: 'gid://gitlab/Milestone/114',
|
||||
title: '14.9',
|
||||
},
|
||||
iteration: {
|
||||
id: 'gid://gitlab/Iteration/124',
|
||||
title: 'Iteration 9',
|
||||
},
|
||||
assignee: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
username: 'admin',
|
||||
},
|
||||
labels: {
|
||||
nodes: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }],
|
||||
},
|
||||
weight: 2,
|
||||
};
|
||||
|
||||
export const mockBoardConfig = {
|
||||
milestoneId: 'gid://gitlab/Milestone/114',
|
||||
milestoneTitle: '14.9',
|
||||
iterationId: 'gid://gitlab/Iteration/124',
|
||||
iterationTitle: 'Iteration 9',
|
||||
assigneeId: 'gid://gitlab/User/1',
|
||||
assigneeUsername: 'admin',
|
||||
labels: [{ id: 'gid://gitlab/Label/32', title: 'Deliverable' }],
|
||||
labelIds: ['gid://gitlab/Label/32'],
|
||||
weight: 2,
|
||||
};
|
||||
|
||||
export const boardObj = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
|
||||
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
|
||||
import {
|
||||
mockBoard,
|
||||
mockBoardConfig,
|
||||
mockLists,
|
||||
mockListsById,
|
||||
mockIssue,
|
||||
|
|
@ -60,6 +62,52 @@ beforeEach(() => {
|
|||
window.gon = { features: {} };
|
||||
});
|
||||
|
||||
describe('fetchBoard', () => {
|
||||
const payload = {
|
||||
fullPath: 'gitlab-org',
|
||||
fullBoardId: 'gid://gitlab/Board/1',
|
||||
boardType: 'project',
|
||||
};
|
||||
|
||||
const queryResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
board: mockBoard,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => {
|
||||
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
||||
|
||||
await testAction({
|
||||
action: actions.fetchBoard,
|
||||
payload,
|
||||
expectedMutations: [
|
||||
{
|
||||
type: types.RECEIVE_BOARD_SUCCESS,
|
||||
payload: mockBoard,
|
||||
},
|
||||
],
|
||||
expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should commit mutation RECEIVE_BOARD_FAILURE on failure', async () => {
|
||||
jest.spyOn(gqlClient, 'query').mockResolvedValue(Promise.reject());
|
||||
|
||||
await testAction({
|
||||
action: actions.fetchBoard,
|
||||
payload,
|
||||
expectedMutations: [
|
||||
{
|
||||
type: types.RECEIVE_BOARD_FAILURE,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInitialBoardData', () => {
|
||||
it('sets data object', () => {
|
||||
const mockData = {
|
||||
|
|
@ -67,13 +115,21 @@ describe('setInitialBoardData', () => {
|
|||
bar: 'baz',
|
||||
};
|
||||
|
||||
return testAction(
|
||||
actions.setInitialBoardData,
|
||||
mockData,
|
||||
{},
|
||||
[{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
|
||||
[],
|
||||
);
|
||||
return testAction({
|
||||
action: actions.setInitialBoardData,
|
||||
payload: mockData,
|
||||
expectedMutations: [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBoardConfig', () => {
|
||||
it('sets board config object from board object', () => {
|
||||
return testAction({
|
||||
action: actions.setBoardConfig,
|
||||
payload: mockBoard,
|
||||
expectedMutations: [{ type: types.SET_BOARD_CONFIG, payload: mockBoardConfig }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +143,7 @@ describe('setFilters', () => {
|
|||
},
|
||||
],
|
||||
[
|
||||
"and use 'assigneeWildcardId' as filter variable for 'assigneId' param",
|
||||
"and use 'assigneeWildcardId' as filter variable for 'assigneeId' param",
|
||||
{
|
||||
filters: { assigneeId: 'None' },
|
||||
filterVariables: { assigneeWildcardId: 'NONE', not: {} },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as types from '~/boards/stores/mutation_types';
|
|||
import mutations from '~/boards/stores/mutations';
|
||||
import defaultState from '~/boards/stores/state';
|
||||
import {
|
||||
mockBoard,
|
||||
mockLists,
|
||||
rawIssue,
|
||||
mockIssue,
|
||||
|
|
@ -33,6 +34,27 @@ describe('Board Store Mutations', () => {
|
|||
state = defaultState();
|
||||
});
|
||||
|
||||
describe('RECEIVE_BOARD_SUCCESS', () => {
|
||||
it('Should set board to state', () => {
|
||||
mutations[types.RECEIVE_BOARD_SUCCESS](state, mockBoard);
|
||||
|
||||
expect(state.board).toEqual({
|
||||
...mockBoard,
|
||||
labels: mockBoard.labels.nodes,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_BOARD_FAILURE', () => {
|
||||
it('Should set error in state', () => {
|
||||
mutations[types.RECEIVE_BOARD_FAILURE](state);
|
||||
|
||||
expect(state.error).toEqual(
|
||||
'An error occurred while fetching the board. Please reload the page.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_INITIAL_BOARD_DATA', () => {
|
||||
it('Should set initial Boards data to state', () => {
|
||||
const allowSubEpics = true;
|
||||
|
|
@ -40,9 +62,6 @@ describe('Board Store Mutations', () => {
|
|||
const fullPath = 'gitlab-org';
|
||||
const boardType = 'group';
|
||||
const disabled = false;
|
||||
const boardConfig = {
|
||||
milestoneTitle: 'Milestone 1',
|
||||
};
|
||||
const issuableType = issuableTypes.issue;
|
||||
|
||||
mutations[types.SET_INITIAL_BOARD_DATA](state, {
|
||||
|
|
@ -51,7 +70,6 @@ describe('Board Store Mutations', () => {
|
|||
fullPath,
|
||||
boardType,
|
||||
disabled,
|
||||
boardConfig,
|
||||
issuableType,
|
||||
});
|
||||
|
||||
|
|
@ -60,11 +78,23 @@ describe('Board Store Mutations', () => {
|
|||
expect(state.fullPath).toEqual(fullPath);
|
||||
expect(state.boardType).toEqual(boardType);
|
||||
expect(state.disabled).toEqual(disabled);
|
||||
expect(state.boardConfig).toEqual(boardConfig);
|
||||
expect(state.issuableType).toEqual(issuableType);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_BOARD_CONFIG', () => {
|
||||
it('Should set board config data o state', () => {
|
||||
const boardConfig = {
|
||||
milestoneId: 1,
|
||||
milestoneTitle: 'Milestone 1',
|
||||
};
|
||||
|
||||
mutations[types.SET_BOARD_CONFIG](state, boardConfig);
|
||||
|
||||
expect(state.boardConfig).toEqual(boardConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
|
||||
it('Should set boardLists to state', () => {
|
||||
mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, initialBoardListsState);
|
||||
|
|
|
|||
|
|
@ -181,12 +181,13 @@ describe('init markdown', () => {
|
|||
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
|
||||
${'- [x] item'} | ${'- [x] item\n- [x] '}
|
||||
${'- item\n - second'} | ${'- item\n - second\n - '}
|
||||
${'1. item'} | ${'1. item\n1. '}
|
||||
${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '}
|
||||
${'1. [x] item'} | ${'1. [x] item\n1. [x] '}
|
||||
${'108. item'} | ${'108. item\n108. '}
|
||||
${'1. item'} | ${'1. item\n2. '}
|
||||
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
|
||||
${'1. [x] item'} | ${'1. [x] item\n2. [x] '}
|
||||
${'108. item'} | ${'108. item\n109. '}
|
||||
${'108. item\n - second'} | ${'108. item\n - second\n - '}
|
||||
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '}
|
||||
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
|
||||
${'non-item, will not change'} | ${'non-item, will not change'}
|
||||
`('adds correct list continuation characters', ({ text, expected }) => {
|
||||
textArea.value = text;
|
||||
textArea.setSelectionRange(text.length, text.length);
|
||||
|
|
@ -207,10 +208,10 @@ describe('init markdown', () => {
|
|||
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
|
||||
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
|
||||
${'- item\n - second\n - '} | ${'- item\n - second\n'}
|
||||
${'1. item\n1. '} | ${'1. item\n'}
|
||||
${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'}
|
||||
${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'}
|
||||
${'108. item\n108. '} | ${'108. item\n'}
|
||||
${'1. item\n2. '} | ${'1. item\n'}
|
||||
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
|
||||
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
|
||||
${'108. item\n109. '} | ${'108. item\n'}
|
||||
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
|
||||
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
|
||||
`('adds correct list continuation characters', ({ text, expected }) => {
|
||||
|
|
@ -243,6 +244,23 @@ describe('init markdown', () => {
|
|||
expect(textArea.value).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each`
|
||||
text | add_at | expected
|
||||
${'1. one\n2. two\n3. three'} | ${13} | ${'1. one\n2. two\n2. \n3. three'}
|
||||
${'108. item\n 5. second\n 6. six\n 7. seven'} | ${36} | ${'108. item\n 5. second\n 6. six\n 6. \n 7. seven'}
|
||||
`(
|
||||
'adds correct numbered continuation characters when in middle of list',
|
||||
({ text, add_at, expected }) => {
|
||||
textArea.value = text;
|
||||
textArea.setSelectionRange(add_at, add_at);
|
||||
|
||||
textArea.addEventListener('keydown', keypressNoteText);
|
||||
textArea.dispatchEvent(enterEvent);
|
||||
|
||||
expect(textArea.value).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('does nothing if feature flag disabled', () => {
|
||||
gon.features = { markdownContinueLists: false };
|
||||
|
||||
|
|
@ -262,8 +280,8 @@ describe('init markdown', () => {
|
|||
});
|
||||
|
||||
describe('with selection', () => {
|
||||
const text = 'initial selected value';
|
||||
const selected = 'selected';
|
||||
let text = 'initial selected value';
|
||||
let selected = 'selected';
|
||||
let selectedIndex;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -409,6 +427,46 @@ describe('init markdown', () => {
|
|||
expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('adds block tags on line above and below selection', () => {
|
||||
selected = 'this text\nis multiple\nlines';
|
||||
text = `before \n${selected}\nafter `;
|
||||
|
||||
textArea.value = text;
|
||||
selectedIndex = text.indexOf(selected);
|
||||
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
|
||||
|
||||
insertMarkdownText({
|
||||
textArea,
|
||||
text,
|
||||
tag: '',
|
||||
blockTag: '***',
|
||||
selected,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`before \n***\n${selected}\n***\nafter `);
|
||||
});
|
||||
|
||||
it('removes block tags on line above and below selection', () => {
|
||||
selected = 'this text\nis multiple\nlines';
|
||||
text = `before \n***\n${selected}\n***\nafter `;
|
||||
|
||||
textArea.value = text;
|
||||
selectedIndex = text.indexOf(selected);
|
||||
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
|
||||
|
||||
insertMarkdownText({
|
||||
textArea,
|
||||
text,
|
||||
tag: '',
|
||||
blockTag: '***',
|
||||
selected,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
expect(textArea.value).toEqual(`before \n${selected}\nafter `);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -460,7 +518,31 @@ describe('init markdown', () => {
|
|||
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`***\n${selected}\n***\n`, undefined);
|
||||
});
|
||||
|
||||
it('uses ace editor to navigate back tag length when nothing is selected', () => {
|
||||
it('removes block tags on line above and below selection', () => {
|
||||
const selected = 'this text\nis multiple\nlines';
|
||||
const text = `before\n***\n${selected}\n***\nafter`;
|
||||
|
||||
editor.getSelection = jest.fn().mockReturnValue({
|
||||
startLineNumber: 2,
|
||||
startColumn: 1,
|
||||
endLineNumber: 4,
|
||||
endColumn: 2,
|
||||
setSelectionRange: jest.fn(),
|
||||
});
|
||||
|
||||
insertMarkdownText({
|
||||
text,
|
||||
tag: '',
|
||||
blockTag: '***',
|
||||
selected,
|
||||
wrap: true,
|
||||
editor,
|
||||
});
|
||||
|
||||
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`${selected}\n`, undefined);
|
||||
});
|
||||
|
||||
it('uses editor to navigate back tag length when nothing is selected', () => {
|
||||
editor.getSelection = jest.fn().mockReturnValue({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
|
|
@ -480,7 +562,7 @@ describe('init markdown', () => {
|
|||
expect(editor.moveCursor).toHaveBeenCalledWith(-1);
|
||||
});
|
||||
|
||||
it('ace editor does not navigate back when there is selected text', () => {
|
||||
it('editor does not navigate back when there is selected text', () => {
|
||||
insertMarkdownText({
|
||||
text: editor.getValue,
|
||||
tag: '*',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu
|
|||
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
|
||||
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils';
|
||||
|
||||
jest.mock('highlight.js/lib/core');
|
||||
Vue.use(VueRouter);
|
||||
|
|
@ -36,6 +37,7 @@ describe('Source Viewer component', () => {
|
|||
beforeEach(() => {
|
||||
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
|
||||
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
|
||||
jest.spyOn(sourceViewerUtils, 'wrapLines');
|
||||
|
||||
return createComponent();
|
||||
});
|
||||
|
|
@ -73,6 +75,10 @@ describe('Source Viewer component', () => {
|
|||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => {
|
||||
expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage);
|
||||
});
|
||||
|
||||
it('renders Line Numbers', () => {
|
||||
expect(findLineNumbers().props('lines')).toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils';
|
|||
|
||||
describe('Wrap lines', () => {
|
||||
it.each`
|
||||
input | output
|
||||
${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'}
|
||||
${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`}
|
||||
${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`}
|
||||
${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'}
|
||||
`('returns lines wrapped in spans containing line numbers', ({ input, output }) => {
|
||||
expect(wrapLines(input)).toBe(output);
|
||||
content | language | output
|
||||
${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'}
|
||||
${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`}
|
||||
${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`}
|
||||
${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'}
|
||||
${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'}
|
||||
${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'}
|
||||
`('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => {
|
||||
expect(wrapLines(content, language)).toBe(output);
|
||||
});
|
||||
|
||||
it.each`
|
||||
language
|
||||
${'invalidLanguage>'}
|
||||
${'"invalidLanguage"'}
|
||||
${'<invalidLanguage'}
|
||||
`('returns lines safely without XSS language is not valid', ({ language }) => {
|
||||
expect(wrapLines('<span class="hljs-code">```bash', language)).toBe(
|
||||
'<span id="LC1" lang="" class="hljs-code">```bash',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,14 +48,26 @@ RSpec.describe Gitlab::Git::Wiki do
|
|||
end
|
||||
|
||||
it 'returns the right page' do
|
||||
expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1'
|
||||
expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
|
||||
page = subject.page(title: 'page1', dir: '')
|
||||
expect(page.url_path).to eq 'page1'
|
||||
expect(page.raw_data).to eq 'content'
|
||||
|
||||
page = subject.page(title: 'page1', dir: 'foo')
|
||||
expect(page.url_path).to eq 'foo/page1'
|
||||
expect(page.raw_data).to eq 'content foo/page1'
|
||||
end
|
||||
|
||||
it 'returns nil for invalid arguments' do
|
||||
expect(subject.page(title: '')).to be_nil
|
||||
expect(subject.page(title: 'foo', version: ':')).to be_nil
|
||||
end
|
||||
|
||||
it 'does not return content if load_content param is set to false' do
|
||||
page = subject.page(title: 'page1', dir: '', load_content: false)
|
||||
|
||||
expect(page.url_path).to eq 'page1'
|
||||
expect(page.raw_data).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#preview_slug' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe SensitiveSerializableHash do
|
||||
describe '.prevent_from_serialization' do
|
||||
let(:test_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Serialization
|
||||
include SensitiveSerializableHash
|
||||
|
||||
attr_accessor :name, :super_secret
|
||||
|
||||
prevent_from_serialization :super_secret
|
||||
|
||||
def attributes
|
||||
{ 'name' => nil, 'super_secret' => nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:model) { test_class.new }
|
||||
|
||||
it 'does not include the field in serializable_hash' do
|
||||
expect(model.serializable_hash).not_to include('super_secret')
|
||||
end
|
||||
|
||||
context 'unsafe_serialization_hash option' do
|
||||
it 'includes the field in serializable_hash' do
|
||||
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include('super_secret')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prevent_sensitive_fields_from_serializable_hash feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(prevent_sensitive_fields_from_serializable_hash: false)
|
||||
end
|
||||
|
||||
it 'includes the field in serializable_hash' do
|
||||
expect(model.serializable_hash).to include('super_secret')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#serializable_hash' do
|
||||
shared_examples "attr_encrypted attribute" do |klass, attribute_name|
|
||||
context "#{klass.name}\##{attribute_name}" do
|
||||
let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
|
||||
|
||||
it 'has a encrypted_attributes field' do
|
||||
expect(klass.encrypted_attributes).to include(attribute_name.to_sym)
|
||||
end
|
||||
|
||||
it 'does not include the attribute in serializable_hash', :aggregate_failures do
|
||||
attributes.each do |attribute|
|
||||
expect(model.attributes).to include(attribute) # double-check the attribute does exist
|
||||
|
||||
expect(model.serializable_hash).not_to include(attribute)
|
||||
expect(model.to_json).not_to include(attribute)
|
||||
expect(model.as_json).not_to include(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unsafe_serialization_hash option' do
|
||||
it 'includes the field in serializable_hash' do
|
||||
attributes.each do |attribute|
|
||||
expect(model.attributes).to include(attribute) # double-check the attribute does exist
|
||||
|
||||
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
|
||||
expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
|
||||
expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'attr_encrypted attribute', WebHook, 'token' do
|
||||
let_it_be(:model) { create(:system_hook) }
|
||||
end
|
||||
|
||||
it_behaves_like 'attr_encrypted attribute', Ci::InstanceVariable, 'value' do
|
||||
let_it_be(:model) { create(:ci_instance_variable) }
|
||||
end
|
||||
|
||||
shared_examples "add_authentication_token_field attribute" do |klass, attribute_name, encrypted_attribute: true, digest_attribute: false|
|
||||
context "#{klass.name}\##{attribute_name}" do
|
||||
let(:attributes) do
|
||||
if digest_attribute
|
||||
["#{attribute_name}_digest"]
|
||||
elsif encrypted_attribute
|
||||
[attribute_name, "#{attribute_name}_encrypted"]
|
||||
else
|
||||
[attribute_name]
|
||||
end
|
||||
end
|
||||
|
||||
it 'has a add_authentication_token_field field' do
|
||||
expect(klass.token_authenticatable_fields).to include(attribute_name.to_sym)
|
||||
end
|
||||
|
||||
it 'does not include the attribute in serializable_hash', :aggregate_failures do
|
||||
attributes.each do |attribute|
|
||||
expect(model.attributes).to include(attribute) # double-check the attribute does exist
|
||||
|
||||
expect(model.serializable_hash).not_to include(attribute)
|
||||
expect(model.to_json).not_to include(attribute)
|
||||
expect(model.as_json).not_to include(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unsafe_serialization_hash option' do
|
||||
it 'includes the field in serializable_hash' do
|
||||
attributes.each do |attribute|
|
||||
expect(model.attributes).to include(attribute) # double-check the attribute does exist
|
||||
|
||||
expect(model.serializable_hash(unsafe_serialization_hash: true)).to include(attribute)
|
||||
expect(model.to_json(unsafe_serialization_hash: true)).to include(attribute)
|
||||
expect(model.as_json(unsafe_serialization_hash: true)).to include(attribute)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'add_authentication_token_field attribute', Ci::Runner, 'token' do
|
||||
let_it_be(:model) { create(:ci_runner) }
|
||||
|
||||
it 'does not include token_expires_at in serializable_hash' do
|
||||
attribute = 'token_expires_at'
|
||||
|
||||
expect(model.attributes).to include(attribute) # double-check the attribute does exist
|
||||
|
||||
expect(model.serializable_hash).not_to include(attribute)
|
||||
expect(model.to_json).not_to include(attribute)
|
||||
expect(model.as_json).not_to include(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'add_authentication_token_field attribute', ApplicationSetting, 'health_check_access_token', encrypted_attribute: false do
|
||||
# health_check_access_token_encrypted column does not exist
|
||||
let_it_be(:model) { create(:application_setting) }
|
||||
end
|
||||
|
||||
it_behaves_like 'add_authentication_token_field attribute', PersonalAccessToken, 'token', encrypted_attribute: false, digest_attribute: true do
|
||||
# PersonalAccessToken only has token_digest column
|
||||
let_it_be(:model) { create(:personal_access_token) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,12 @@ RSpec.shared_examples 'TokenAuthenticatable' do
|
|||
it { is_expected.to respond_to("set_#{token_field}") }
|
||||
it { is_expected.to respond_to("reset_#{token_field}!") }
|
||||
end
|
||||
|
||||
describe 'SensitiveSerializableHash' do
|
||||
it 'includes the token field in list of sensitive attributes prevented from serialization' do
|
||||
expect(described_class.attributes_exempt_from_serializable_hash).to include(token_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe User, 'TokenAuthenticatable' do
|
||||
|
|
|
|||
|
|
@ -6,6 +6,24 @@ RSpec.describe TokenAuthenticatableStrategies::Base do
|
|||
let(:instance) { double(:instance) }
|
||||
let(:field) { double(:field) }
|
||||
|
||||
describe '#token_fields' do
|
||||
let(:strategy) { described_class.new(instance, field, options) }
|
||||
let(:field) { 'some_token' }
|
||||
let(:options) { {} }
|
||||
|
||||
it 'includes the token field' do
|
||||
expect(strategy.token_fields).to contain_exactly(field)
|
||||
end
|
||||
|
||||
context 'with expires_at option' do
|
||||
let(:options) { { expires_at: true } }
|
||||
|
||||
it 'includes the token_expires_at field' do
|
||||
expect(strategy.token_fields).to contain_exactly(field, 'some_token_expires_at')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fabricate' do
|
||||
context 'when digest stragegy is specified' do
|
||||
it 'fabricates digest strategy object' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe TokenAuthenticatableStrategies::Digest do
|
||||
let(:model) { class_double('Project') }
|
||||
let(:options) { { digest: true } }
|
||||
|
||||
subject(:strategy) do
|
||||
described_class.new(model, 'some_field', options)
|
||||
end
|
||||
|
||||
describe '#token_fields' do
|
||||
it 'includes the digest field' do
|
||||
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_digest')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,10 +14,18 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
|
|||
Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
|
||||
end
|
||||
|
||||
subject do
|
||||
subject(:strategy) do
|
||||
described_class.new(model, 'some_field', options)
|
||||
end
|
||||
|
||||
describe '#token_fields' do
|
||||
let(:options) { { encrypted: :required } }
|
||||
|
||||
it 'includes the encrypted field' do
|
||||
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_token_authenticatable' do
|
||||
context 'when encryption is required' do
|
||||
let(:options) { { encrypted: :required } }
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
|
|||
end
|
||||
|
||||
expect(subject[:status]).to eq(:success)
|
||||
expect(job.job_variables.as_json).to contain_exactly(
|
||||
expect(job.job_variables.as_json(only: [:key, :value, :source])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'),
|
||||
hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv'))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the artifact' do
|
||||
expect(subject[:status]).to eq(:success)
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
|
||||
hash_including('key' => 'KEY2', 'value' => 'VAR2'))
|
||||
end
|
||||
|
|
@ -57,7 +57,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
|
||||
expect(subject[:status]).to eq(:success)
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR4'),
|
||||
hash_including('key' => 'KEY2', 'value' => 'VAR3'))
|
||||
end
|
||||
|
|
@ -101,7 +101,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'trims the trailing space' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR1'))
|
||||
end
|
||||
end
|
||||
|
|
@ -112,7 +112,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the dotenv data' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS'))
|
||||
end
|
||||
end
|
||||
|
|
@ -133,7 +133,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the dotenv data' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'skateboard', 'value' => '🛹'))
|
||||
end
|
||||
end
|
||||
|
|
@ -154,7 +154,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the dotenv data' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'V A R 1'))
|
||||
end
|
||||
end
|
||||
|
|
@ -165,7 +165,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the value as-is' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => '"VAR1"'))
|
||||
end
|
||||
end
|
||||
|
|
@ -176,7 +176,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the value as-is' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => "'VAR1'"))
|
||||
end
|
||||
end
|
||||
|
|
@ -187,7 +187,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the value as-is' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => '" VAR1 "'))
|
||||
end
|
||||
end
|
||||
|
|
@ -208,7 +208,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'parses the dotenv data' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => ''))
|
||||
end
|
||||
end
|
||||
|
|
@ -250,7 +250,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'does not support variable expansion in dotenv parser' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
|
||||
hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test'))
|
||||
end
|
||||
|
|
@ -284,7 +284,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
|||
it 'does not support comment in dotenv parser' do
|
||||
subject
|
||||
|
||||
expect(build.job_variables.as_json).to contain_exactly(
|
||||
expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
|
||||
hash_including('key' => 'KEY1', 'value' => 'VAR1 # This is variable'))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ module Tooling
|
|||
---
|
||||
stage: Ecosystem
|
||||
group: Integrations
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
<!---
|
||||
|
|
|
|||
Loading…
Reference in New Issue