Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-22 09:11:23 +00:00
parent dc90a96501
commit 535401c636
72 changed files with 487 additions and 325 deletions

View File

@ -3,6 +3,7 @@ extends:
- plugin:@gitlab/i18n - plugin:@gitlab/i18n
- plugin:no-jquery/slim - plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4 - plugin:no-jquery/deprecated-3.4
- plugin:no-unsanitized/DOM
- ./tooling/eslint-config/conditionally_ignore.js - ./tooling/eslint-config/conditionally_ignore.js
globals: globals:
__webpack_public_path__: true __webpack_public_path__: true
@ -116,6 +117,14 @@ rules:
vue/multi-word-component-names: off vue/multi-word-component-names: off
unicorn/prefer-dom-node-dataset: unicorn/prefer-dom-node-dataset:
- error - error
no-unsanitized/method:
- error
- escape:
methods: 'sanitize'
no-unsanitized/property:
- error
- escape:
methods: 'sanitize'
overrides: overrides:
- files: - files:
- '{,ee/,jh/}spec/frontend*/**/*' - '{,ee/,jh/}spec/frontend*/**/*'
@ -134,6 +143,8 @@ overrides:
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.' message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
- selector: ImportSpecifier[imported.name='GlSkeletonLoading'] - selector: ImportSpecifier[imported.name='GlSkeletonLoading']
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.' message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
no-unsanitized/method: off
no-unsanitized/property: off
- files: - files:
- 'config/**/*' - 'config/**/*'
- 'scripts/**/*' - 'scripts/**/*'

View File

@ -482,7 +482,7 @@ gem 'net-ntp'
gem 'ssh_data', '~> 1.3' gem 'ssh_data', '~> 1.3'
# Spamcheck GRPC protocol definitions # Spamcheck GRPC protocol definitions
gem 'spamcheck', '~> 0.1.0' gem 'spamcheck', '~> 1.0.0'
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 15.3.0-rc4' gem 'gitaly', '~> 15.3.0-rc4'

View File

@ -1314,7 +1314,7 @@ GEM
sorted_set (1.0.3) sorted_set (1.0.3)
rbtree rbtree
set (~> 1.0) set (~> 1.0)
spamcheck (0.1.0) spamcheck (1.0.0)
grpc (~> 1.0) grpc (~> 1.0)
spring (2.1.1) spring (2.1.1)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
@ -1746,7 +1746,7 @@ DEPENDENCIES
slack-messenger (~> 2.3.4) slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.6.1) snowplow-tracker (~> 0.6.1)
solargraph (~> 0.45.0) solargraph (~> 0.45.0)
spamcheck (~> 0.1.0) spamcheck (~> 1.0.0)
spring (~> 2.1.0) spring (~> 2.1.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
sprite-factory (~> 1.7) sprite-factory (~> 1.7)

View File

@ -165,6 +165,7 @@ export class AwardsHandler {
`; `;
const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body; const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
// eslint-disable-next-line no-unsanitized/method
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup); targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories(); this.addRemainingEmojiMenuCategories();
@ -198,6 +199,7 @@ export class AwardsHandler {
emojisInCategory, emojisInCategory,
); );
requestAnimationFrame(() => { requestAnimationFrame(() => {
// eslint-disable-next-line no-unsanitized/method
emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup); emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
resolve(); resolve();
}); });

View File

@ -67,6 +67,7 @@ export default {
}, },
content() { content() {
const el = document.createElement('div'); const el = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = this.draft.note_html; el.innerHTML = this.draft.note_html;
return el.textContent; return el.textContent;

View File

@ -22,6 +22,7 @@ class CopyCodeButton extends HTMLElement {
'data-clipboard-target': `pre#${this.for}`, 'data-clipboard-target': `pre#${this.for}`,
}); });
// eslint-disable-next-line no-unsanitized/property
button.innerHTML = spriteIcon('copy-to-clipboard'); button.innerHTML = spriteIcon('copy-to-clipboard');
return button; return button;

View File

@ -91,6 +91,7 @@ class SafeMathRenderer {
`; `;
if (!wrapperElement.classList.contains('lazy-alert-shown')) { if (!wrapperElement.classList.contains('lazy-alert-shown')) {
// eslint-disable-next-line no-unsanitized/property
wrapperElement.innerHTML = html; wrapperElement.innerHTML = html;
wrapperElement.append(codeElement); wrapperElement.append(codeElement);
wrapperElement.classList.add('lazy-alert-shown'); wrapperElement.classList.add('lazy-alert-shown');
@ -111,6 +112,7 @@ class SafeMathRenderer {
} }
try { try {
// eslint-disable-next-line no-unsanitized/property
displayContainer.innerHTML = this.katex.renderToString(text, { displayContainer.innerHTML = this.katex.renderToString(text, {
displayMode: el.dataset.mathStyle === 'display', displayMode: el.dataset.mathStyle === 'display',
throwOnError: true, throwOnError: true,

View File

@ -45,6 +45,7 @@ const loadViewer = (viewerParam) => {
viewer.dataset.loading = 'true'; viewer.dataset.loading = 'true';
return axios.get(url).then(({ data }) => { return axios.get(url).then(({ data }) => {
// eslint-disable-next-line no-unsanitized/property
viewer.innerHTML = data.html; viewer.innerHTML = data.html;
window.requestIdleCallback(() => { window.requestIdleCallback(() => {

View File

@ -23,6 +23,7 @@ const wrapTextWithSpan = (el, text) => {
const wrapNodes = (text) => { const wrapNodes = (text) => {
const wrapper = createSpan(); const wrapper = createSpan();
// eslint-disable-next-line no-unsanitized/property
wrapper.innerHTML = wrapSpacesWithSpans(text); wrapper.innerHTML = wrapSpacesWithSpans(text);
wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text)); wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
return wrapper.childNodes; return wrapper.childNodes;

View File

@ -13,6 +13,7 @@ const renderersByType = {
}, },
header(element, data) { header(element, data) {
element.classList.add('dropdown-header'); element.classList.add('dropdown-header');
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = data.content; element.innerHTML = data.content;
return element; return element;
@ -122,6 +123,7 @@ function assignTextToLink(el, data, options) {
const text = getLinkText(data, options); const text = getLinkText(data, options);
if (options.icon || options.highlight) { if (options.icon || options.highlight) {
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = text; el.innerHTML = text;
} else { } else {
el.textContent = text; el.textContent = text;

View File

@ -81,6 +81,7 @@ export default class FilterableList {
onFilterSuccess(response, queryData) { onFilterSuccess(response, queryData) {
if (response.data.html) { if (response.data.html) {
// eslint-disable-next-line no-unsanitized/property
this.listHolderElement.innerHTML = response.data.html; this.listHolderElement.innerHTML = response.data.html;
} }

View File

@ -75,6 +75,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
const name = valueElement.innerText; const name = valueElement.innerText;
const emojiTag = this.glEmojiTag(name); const emojiTag = this.glEmojiTag(name);
const emojiElement = dropdownItem.querySelector('gl-emoji'); const emojiElement = dropdownItem.querySelector('gl-emoji');
// eslint-disable-next-line no-unsanitized/property
emojiElement.outerHTML = emojiTag; emojiElement.outerHTML = emojiTag;
} }
}); });

View File

@ -107,7 +107,7 @@ class DropDown {
} }
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list; const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
// eslint-disable-next-line no-unsanitized/property
renderableList.innerHTML = children.join(''); renderableList.innerHTML = children.join('');
const listEvent = new CustomEvent('render.dl', { const listEvent = new CustomEvent('render.dl', {
@ -121,7 +121,7 @@ class DropDown {
renderChildren(data) { renderChildren(data) {
const html = utils.template(this.templateString, data); const html = utils.template(this.templateString, data);
const template = document.createElement('div'); const template = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = html; template.innerHTML = html;
DropDown.setImagesSrc(template); DropDown.setImagesSrc(template);
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block'; template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';

View File

@ -42,6 +42,7 @@ class HookButton extends Hook {
} }
restoreInitialState() { restoreInitialState() {
// eslint-disable-next-line no-unsanitized/property
this.list.list.innerHTML = this.list.initialState; this.list.list.innerHTML = this.list.initialState;
} }

View File

@ -97,6 +97,7 @@ class HookInput extends Hook {
} }
restoreInitialState() { restoreInitialState() {
// eslint-disable-next-line no-unsanitized/property
this.list.list.innerHTML = this.list.initialState; this.list.list.innerHTML = this.list.initialState;
} }

View File

@ -122,6 +122,7 @@ export default class FilteredSearchVisualTokens {
const hasOperator = Boolean(operator); const hasOperator = Boolean(operator);
if (value) { if (value) {
// eslint-disable-next-line no-unsanitized/property
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
canEdit, canEdit,
uppercaseTokenName, uppercaseTokenName,
@ -138,6 +139,7 @@ export default class FilteredSearchVisualTokens {
operatorHTML = '<div class="operator"></div>'; operatorHTML = '<div class="operator"></div>';
} }
// eslint-disable-next-line no-unsanitized/property
li.innerHTML = nameHTML + operatorHTML; li.innerHTML = nameHTML + operatorHTML;
} }
@ -160,6 +162,8 @@ export default class FilteredSearchVisualTokens {
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) { if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial(); const name = FilteredSearchVisualTokens.getLastTokenPartial();
const operator = FilteredSearchVisualTokens.getLastTokenOperator(); const operator = FilteredSearchVisualTokens.getLastTokenOperator();
// eslint-disable-next-line no-unsanitized/property
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
hasOperator: Boolean(operator), hasOperator: Boolean(operator),
}); });
@ -293,6 +297,7 @@ export default class FilteredSearchVisualTokens {
const button = lastVisualToken.querySelector('.selectable'); const button = lastVisualToken.querySelector('.selectable');
const valueContainer = lastVisualToken.querySelector('.value-container'); const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer); button.removeChild(valueContainer);
// eslint-disable-next-line no-unsanitized/property
lastVisualToken.innerHTML = button.innerHTML; lastVisualToken.innerHTML = button.innerHTML;
} else if (operator) { } else if (operator) {
lastVisualToken.removeChild(operator); lastVisualToken.removeChild(operator);

View File

@ -47,6 +47,7 @@ export default class VisualTokenValue {
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue; tokenValueContainer.dataset.originalValue = tokenValue;
// eslint-disable-next-line no-unsanitized/property
tokenValueElement.innerHTML = ` tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt=""> <img class="avatar s20" src="${user.avatar_url}" alt="">
${escape(user.name)} ${escape(user.name)}
@ -152,6 +153,7 @@ export default class VisualTokenValue {
} }
container.dataset.originalValue = value; container.dataset.originalValue = value;
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = Emoji.glEmojiTag(value); element.innerHTML = Emoji.glEmojiTag(value);
}); });
} }

View File

@ -236,11 +236,13 @@ const createFlash = function createFlash({
if (!flashContainer) return null; if (!flashContainer) return null;
// eslint-disable-next-line no-unsanitized/property
flashContainer.innerHTML = createFlashEl(message, type); flashContainer.innerHTML = createFlashEl(message, type);
const flashEl = flashContainer.querySelector(`.flash-${type}`); const flashEl = flashContainer.querySelector(`.flash-${type}`);
if (actionConfig) { if (actionConfig) {
// eslint-disable-next-line no-unsanitized/method
flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig)); flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
if (actionConfig.clickHandler) { if (actionConfig.clickHandler) {

View File

@ -30,6 +30,7 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
export function addImageCommentBadge(containerEl, { coordinate, noteId }) { export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']); const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']);
// eslint-disable-next-line no-unsanitized/property
buttonEl.innerHTML = spriteIcon('image-comment-dark'); buttonEl.innerHTML = spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl); containerEl.appendChild(buttonEl);

View File

@ -8,6 +8,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
buttonEl.style.left = `${x}px`; buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`; buttonEl.style.top = `${y}px`;
// eslint-disable-next-line no-unsanitized/property
buttonEl.innerHTML = spriteIcon('image-comment-dark'); buttonEl.innerHTML = spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl); containerEl.appendChild(buttonEl);

View File

@ -226,6 +226,7 @@ export default {
}, },
createDragIconElement() { createDragIconElement() {
const container = document.createElement('div'); const container = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true"> container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
<use href="${gon.sprite_icons}#drag-vertical"></use> <use href="${gon.sprite_icons}#drag-vertical"></use>
</svg>`; </svg>`;
@ -358,6 +359,7 @@ export default {
); );
button.id = `js-task-button-${index}`; button.id = `js-task-button-${index}`;
this.taskButtons.push(button.id); this.taskButtons.push(button.id);
// eslint-disable-next-line no-unsanitized/property
button.innerHTML = ` button.innerHTML = `
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"> <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
<use href="${gon.sprite_icons}#doc-new"></use> <use href="${gon.sprite_icons}#doc-new"></use>

View File

@ -13,6 +13,7 @@ const updateDescription = (descriptionHtml = '', details) => {
} }
const placeholder = document.createElement('div'); const placeholder = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
placeholder.innerHTML = descriptionHtml; placeholder.innerHTML = descriptionHtml;
const newDetails = placeholder.getElementsByTagName('details'); const newDetails = placeholder.getElementsByTagName('details');

View File

@ -247,6 +247,7 @@ export default class LabelsSelect {
} }
linkEl.className = selectedClass.join(' '); linkEl.className = selectedClass.join(' ');
// eslint-disable-next-line no-unsanitized/property
linkEl.innerHTML = `${colorEl} ${escape(label.title)}`; linkEl.innerHTML = `${colorEl} ${escape(label.title)}`;
const listItemEl = document.createElement('li'); const listItemEl = document.createElement('li');

View File

@ -9,6 +9,7 @@ const setIframeRenderedSize = (h, w) => {
const drawDiagram = (source) => { const drawDiagram = (source) => {
const element = document.getElementById('app'); const element = document.getElementById('app');
const insertSvg = (svgCode) => { const insertSvg = (svgCode) => {
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = svgCode; element.innerHTML = svgCode;
const height = parseInt(element.firstElementChild.getAttribute('height'), 10); const height = parseInt(element.firstElementChild.getAttribute('height'), 10);

View File

@ -119,6 +119,7 @@ const getAverageCharWidth = memoize(function getAverageCharWidth(options = {}) {
div.style.left = -1000; div.style.left = -1000;
div.style.top = -1000; div.style.top = -1000;
// eslint-disable-next-line no-unsanitized/property
div.innerHTML = chars; div.innerHTML = chars;
document.body.appendChild(div); document.body.appendChild(div);

View File

@ -434,6 +434,7 @@ export default class MergeRequestTabs {
.get(`${source}.json`) .get(`${source}.json`)
.then(({ data }) => { .then(({ data }) => {
const commitsDiv = document.querySelector('div#commits'); const commitsDiv = document.querySelector('div#commits');
// eslint-disable-next-line no-unsanitized/property
commitsDiv.innerHTML = data.html; commitsDiv.innerHTML = data.html;
localTimeAgo(commitsDiv.querySelectorAll('.js-timeago')); localTimeAgo(commitsDiv.querySelectorAll('.js-timeago'));
this.commitsLoaded = true; this.commitsLoaded = true;

View File

@ -63,6 +63,8 @@ export default class PayloadPreviewer {
insertPayload(data) { insertPayload(data) {
this.isInserted = true; this.isInserted = true;
// eslint-disable-next-line no-unsanitized/property
this.getContainer().innerHTML = data; this.getContainer().innerHTML = data;
this.showPayload(); this.showPayload();
} }

View File

@ -200,9 +200,11 @@ export default class Todos {
}); });
document.dispatchEvent(event); document.dispatchEvent(event);
// eslint-disable-next-line no-unsanitized/property
document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter( document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
data.count, data.count,
); );
// eslint-disable-next-line no-unsanitized/property
document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter( document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
data.done_count, data.done_count,
); );

View File

@ -29,6 +29,7 @@ const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji; statusEmojiField.value = emoji;
toggleNoEmojiPlaceholder(false); toggleNoEmojiPlaceholder(false);
removeStatusEmoji(); removeStatusEmoji();
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += emojiTag; toggleEmojiMenuButton.innerHTML += emojiTag;
}; };
@ -74,6 +75,7 @@ Emoji.initEmojiMap()
if (hasStatusMessage) { if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false); toggleNoEmojiPlaceholder(false);
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += defaultEmojiTag; toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) { } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true); toggleNoEmojiPlaceholder(true);

View File

@ -8,7 +8,10 @@ const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSk
if (skippable) { if (skippable) {
const button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; const button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert'); const flashAlert = document.querySelector('.flash-alert');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); if (flashAlert) {
// eslint-disable-next-line no-unsanitized/method
flashAlert.insertAdjacentHTML('beforeend', button);
}
} }
mount2faRegistration(); mount2faRegistration();

View File

@ -33,6 +33,7 @@ export default class UserOverviewBlock {
const containerEl = document.querySelector(this.container); const containerEl = document.querySelector(this.container);
const contentList = containerEl.querySelector('.overview-content-list'); const contentList = containerEl.querySelector('.overview-content-list');
// eslint-disable-next-line no-unsanitized/property
contentList.innerHTML += html; contentList.innerHTML += html;
const loadingEl = containerEl.querySelector('.loading'); const loadingEl = containerEl.querySelector('.loading');

View File

@ -26,6 +26,7 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) {
if (reason) { if (reason) {
const optionTitle = option.querySelector('.js-visibility-level-radio span'); const optionTitle = option.querySelector('.js-visibility-level-radio span');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : ''; const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
// eslint-disable-next-line no-unsanitized/property
reason.innerHTML = sprintf( reason.innerHTML = sprintf(
__( __(
'This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group.', 'This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group.',

View File

@ -22,11 +22,14 @@ export default class Star {
starSpan.classList.remove('starred'); starSpan.classList.remove('starred');
starSpan.textContent = s__('StarProject|Star'); starSpan.textContent = s__('StarProject|Star');
starIcon.remove(); starIcon.remove();
// eslint-disable-next-line no-unsanitized/method
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses)); starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
} else { } else {
starSpan.classList.add('starred'); starSpan.classList.add('starred');
starSpan.textContent = __('Unstar'); starSpan.textContent = __('Unstar');
starIcon.remove(); starIcon.remove();
// eslint-disable-next-line no-unsanitized/method
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses)); starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
} }
}) })

View File

@ -49,6 +49,9 @@ export default {
error, error,
}); });
}, },
context: {
isSingleRequest: true,
},
}, },
}, },
computed: { computed: {

View File

@ -19,6 +19,7 @@ export default class InputValidator {
setValidationMessage() { setValidationMessage() {
if (this.invalidInput) { if (this.invalidInput) {
this.inputDomElement.setCustomValidity(this.errorMessage); this.inputDomElement.setCustomValidity(this.errorMessage);
// eslint-disable-next-line no-unsanitized/property
this.inputErrorMessage.innerHTML = this.errorMessage; this.inputErrorMessage.innerHTML = this.errorMessage;
} else { } else {
this.resetValidationMessage(); this.resetValidationMessage();
@ -28,6 +29,7 @@ export default class InputValidator {
resetValidationMessage() { resetValidationMessage() {
if (this.inputDomElement.validationMessage === this.errorMessage) { if (this.inputDomElement.validationMessage === this.errorMessage) {
this.inputDomElement.setCustomValidity(''); this.inputDomElement.setCustomValidity('');
// eslint-disable-next-line no-unsanitized/property
this.inputErrorMessage.innerHTML = this.inputDomElement.title; this.inputErrorMessage.innerHTML = this.inputDomElement.title;
} }
} }

View File

@ -428,6 +428,7 @@ export default {
.then((res) => { .then((res) => {
if (res.data) { if (res.data) {
const el = document.createElement('div'); const el = document.createElement('div');
// eslint-disable-next-line no-unsanitized/property
el.innerHTML = res.data; el.innerHTML = res.data;
document.body.appendChild(el); document.body.appendChild(el);
document.dispatchEvent(new CustomEvent('merged:UpdateActions')); document.dispatchEvent(new CustomEvent('merged:UpdateActions'));

View File

@ -163,6 +163,7 @@ export default {
// resets the container HTML (replaces it with the updated noteHTML) // resets the container HTML (replaces it with the updated noteHTML)
// calls `renderSuggestions` once the updated noteHTML is added to the DOM // calls `renderSuggestions` once the updated noteHTML is added to the DOM
// eslint-disable-next-line no-unsanitized/property
this.$refs.container.innerHTML = this.noteHtml; this.$refs.container.innerHTML = this.noteHtml;
this.isRendered = false; this.isRendered = false;
this.renderSuggestions(); this.renderSuggestions();

View File

@ -3,6 +3,8 @@ import { HLJS_COMMENT_SELECTOR } from '../constants';
const createWrapper = (content) => { const createWrapper = (content) => {
const span = document.createElement('span'); const span = document.createElement('span');
span.className = HLJS_COMMENT_SELECTOR; span.className = HLJS_COMMENT_SELECTOR;
// eslint-disable-next-line no-unsanitized/property
span.innerHTML = content; span.innerHTML = content;
return span.outerHTML; return span.outerHTML;
}; };

View File

@ -10,6 +10,7 @@ export default {
mounted() { mounted() {
const legacyEntry = document.querySelector(this.selector); const legacyEntry = document.querySelector(this.selector);
if (legacyEntry.tagName === 'TEMPLATE') { if (legacyEntry.tagName === 'TEMPLATE') {
// eslint-disable-next-line no-unsanitized/property
this.$el.innerHTML = legacyEntry.innerHTML; this.$el.innerHTML = legacyEntry.innerHTML;
} else { } else {
this.source = legacyEntry.parentNode; this.source = legacyEntry.parentNode;

View File

@ -20,6 +20,7 @@ const reloadMessage = LIVE_RELOAD
? 'You have live_reload enabled, the page will reload automatically when complete.' ? 'You have live_reload enabled, the page will reload automatically when complete.'
: 'You have live_reload disabled, the page will reload automatically in a few seconds.'; : 'You have live_reload disabled, the page will reload automatically in a few seconds.';
// eslint-disable-next-line no-unsanitized/property
div.innerHTML = ` div.innerHTML = `
<!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg --> <!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg -->
<svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200"> <svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">

View File

@ -48,7 +48,7 @@
opacity: 0; opacity: 0;
} }
a { a:not(.canary-badge) {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 8px; padding: 2px 8px;
@ -61,10 +61,6 @@
} }
} }
.canary-badge {
margin-left: -8px;
}
.project-item-select { .project-item-select {
right: auto; right: auto;
left: 0; left: 0;

View File

@ -332,9 +332,6 @@ kbd kbd {
color: #fff; color: #fff;
background-color: #c17d10; background-color: #c17d10;
} }
.bg-transparent {
background-color: transparent !important;
}
.rounded-circle { .rounded-circle {
border-radius: 50% !important; border-radius: 50% !important;
} }
@ -815,20 +812,17 @@ kbd {
.navbar-gitlab .header-content .title img { .navbar-gitlab .header-content .title img {
height: 24px; height: 24px;
} }
.navbar-gitlab .header-content .title a { .navbar-gitlab .header-content .title a:not(.canary-badge) {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 8px; padding: 2px 8px;
margin: 4px 2px 4px -8px; margin: 4px 2px 4px -8px;
border-radius: 4px; border-radius: 4px;
} }
.navbar-gitlab .header-content .title a:active { .navbar-gitlab .header-content .title a:not(.canary-badge):active {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
outline: none; outline: none;
} }
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px; margin: 0 2px;
} }

View File

@ -311,9 +311,6 @@ kbd kbd {
color: #fff; color: #fff;
background-color: #ab6100; background-color: #ab6100;
} }
.bg-transparent {
background-color: transparent !important;
}
.rounded-circle { .rounded-circle {
border-radius: 50% !important; border-radius: 50% !important;
} }
@ -794,20 +791,17 @@ kbd {
.navbar-gitlab .header-content .title img { .navbar-gitlab .header-content .title img {
height: 24px; height: 24px;
} }
.navbar-gitlab .header-content .title a { .navbar-gitlab .header-content .title a:not(.canary-badge) {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 8px; padding: 2px 8px;
margin: 4px 2px 4px -8px; margin: 4px 2px 4px -8px;
border-radius: 4px; border-radius: 4px;
} }
.navbar-gitlab .header-content .title a:active { .navbar-gitlab .header-content .title a:not(.canary-badge):active {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
outline: none; outline: none;
} }
.navbar-gitlab .header-content .title .canary-badge {
margin-left: -8px;
}
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px; margin: 0 2px;
} }

View File

@ -67,7 +67,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
return unless current_jira_installation.instance_url? return unless current_jira_installation.instance_url?
request.content_security_policy.directives['connect-src'] ||= [] request.content_security_policy.directives['connect-src'] ||= []
request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_ids') request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_id')
end end
def create_service def create_service

View File

@ -91,7 +91,7 @@ class Snippet < ApplicationRecord
participant :notes_with_associations participant :notes_with_associations
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true attr_spammable :description, spam_description: true
attr_encrypted :secret_token, attr_encrypted :secret_token,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,
@ -276,13 +276,7 @@ class Snippet < ApplicationRecord
def check_for_spam?(user:) def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) || visibility_level_changed?(to: Snippet::PUBLIC) ||
(public? && (title_changed? || content_changed?)) (public? && (title_changed? || description_changed?))
end
# snippets are the biggest sources of spam
override :allow_possible_spam?
def allow_possible_spam?
false
end end
def spammable_entity_type def spammable_entity_type

View File

@ -73,6 +73,15 @@ module Snippets
message message
end end
def file_paths_to_commit
paths = []
snippet_actions.to_commit_actions.each do |action|
paths << { path: action[:file_path] }
end
paths
end
def files_to_commit(snippet) def files_to_commit(snippet)
snippet_actions.to_commit_actions.presence || build_actions_from_params(snippet) snippet_actions.to_commit_actions.presence || build_actions_from_params(snippet)
end end

View File

@ -24,7 +24,8 @@ module Snippets
spammable: @snippet, spammable: @snippet,
spam_params: spam_params, spam_params: spam_params,
user: current_user, user: current_user,
action: :create action: :create,
extra_features: { files: file_paths_to_commit }
).execute ).execute
if save_and_commit if save_and_commit

View File

@ -23,11 +23,14 @@ module Snippets
update_snippet_attributes(snippet) update_snippet_attributes(snippet)
files = snippet.all_files.map { |f| { path: f } } + file_paths_to_commit
Spam::SpamActionService.new( Spam::SpamActionService.new(
spammable: snippet, spammable: snippet,
spam_params: spam_params, spam_params: spam_params,
user: current_user, user: current_user,
action: :update action: :update,
extra_features: { files: files }
).execute ).execute
if save_and_commit(snippet) if save_and_commit(snippet)

View File

@ -4,11 +4,12 @@ module Spam
class SpamActionService class SpamActionService
include SpamConstants include SpamConstants
def initialize(spammable:, spam_params:, user:, action:) def initialize(spammable:, spam_params:, user:, action:, extra_features: {})
@target = spammable @target = spammable
@spam_params = spam_params @spam_params = spam_params
@user = user @user = user
@action = action @action = action
@extra_features = extra_features
end end
# rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/AbcSize
@ -40,7 +41,7 @@ module Spam
private private
attr_reader :user, :action, :target, :spam_params, :spam_log attr_reader :user, :action, :target, :spam_params, :spam_log, :extra_features
## ##
# In order to be proceed to the spam check process, the target must be # In order to be proceed to the spam check process, the target must be
@ -124,7 +125,9 @@ module Spam
SpamVerdictService.new(target: target, SpamVerdictService.new(target: target,
user: user, user: user,
options: options, options: options,
context: context) context: context,
extra_features: extra_features
)
end end
def noteable_type def noteable_type

View File

@ -5,11 +5,12 @@ module Spam
include AkismetMethods include AkismetMethods
include SpamConstants include SpamConstants
def initialize(user:, target:, options:, context: {}) def initialize(user:, target:, options:, context: {}, extra_features: {})
@target = target @target = target
@user = user @user = user
@options = options @options = options
@context = context @context = context
@extra_features = extra_features
end end
def execute def execute
@ -61,7 +62,7 @@ module Spam
private private
attr_reader :user, :target, :options, :context attr_reader :user, :target, :options, :context, :extra_features
def akismet_verdict def akismet_verdict
if akismet.spam? if akismet.spam?
@ -75,7 +76,8 @@ module Spam
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
begin begin
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context) result, attribs, _error = spamcheck_client.spam?(spammable: target, user: user, context: context,
extra_features: extra_features)
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545 # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
return [nil, attribs] unless result return [nil, attribs] unless result

View File

@ -10,10 +10,10 @@
%span.gl-sr-only GitLab %span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do = link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
= brand_header_logo = brand_header_logo
.gl-display-flex.gl-align-items-center
- if Gitlab.com_and_canary? - if Gitlab.com_and_canary?
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do = gl_badge_tag({ variant: :success, size: :sm }, { href: Gitlab::Saas.canary_toggle_com_url, data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer', class: 'canary-badge' }) do
= gl_badge_tag({ variant: :success, size: :sm }) do = _('Next')
= _('Next')
- if current_user - if current_user
.gl-display-none.gl-sm-display-block .gl-display-none.gl-sm-display-block

View File

@ -1,34 +0,0 @@
- name: "Manual iteration management" # The name of the feature to be deprecated
announcement_milestone: "14.10" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-04-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "16.0" # The milestone when this feature is planned to be removed
removal_date: "2023-04-22" # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true
reporter: mcelicalderonG # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
`description` will also be removed.
On the GraphQL API the following mutations will be removed:
1. `iterationCreate`
1. `iterationDelete`
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
arguments will be removed:
1. `title`
1. `dueDate`
1. `startDate`
For more information about iteration cadences, you can refer to
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
# The following items are not published on the docs page, but may be used in the future.
stage: Plan
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356069
documentation_url: # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg

View File

@ -60,7 +60,7 @@ class Gitlab::Seeder::Pipelines
::Ci::ProcessPipelineService.new(pipeline).execute ::Ci::ProcessPipelineService.new(pipeline).execute
end end
::Gitlab::Seeder::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline
end end
private private

View File

@ -361,38 +361,6 @@ In GitLab 15.0, for Dependency Scanning, the default version of Java that the sc
</div> </div>
<div class="deprecation removal-160 breaking-change">
### Manual iteration management
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-04-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
`description` will also be removed.
On the GraphQL API the following mutations will be removed:
1. `iterationCreate`
1. `iterationDelete`
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
arguments will be removed:
1. `title`
1. `dueDate`
1. `startDate`
For more information about iteration cadences, you can refer to
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
</div>
<div class="deprecation removal-150 breaking-change"> <div class="deprecation removal-150 breaking-change">
### Outdated indices of Advanced Search migrations ### Outdated indices of Advanced Search migrations

View File

@ -12,11 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Moved to GitLab Premium in 13.9. > - Moved to GitLab Premium in 13.9.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) in GitLab 14.6. [Feature flag `group_iterations`](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) removed. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) in GitLab 14.6. [Feature flag `group_iterations`](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) removed.
WARNING:
After [Iteration Cadences](#iteration-cadences) becomes generally available,
manual iteration scheduling will be [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 15.6.
To enhance the role of iterations as time boundaries, we will also deprecate the title field.
Iterations are a way to track issues over a period of time. This allows teams Iterations are a way to track issues over a period of time. This allows teams
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md) to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
for tracking over different time periods. for tracking over different time periods.
@ -187,7 +182,7 @@ To group issues by label:
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1 [with a flag](../../../administration/feature_flags.md), named `iteration_cadences`. Disabled by default. > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1 [with a flag](../../../administration/feature_flags.md), named `iteration_cadences`. Disabled by default.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts. > - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/354878) in GitLab 15.0. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/354878) in GitLab 15.0.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.3: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date. > - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.4: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date. Iteration cadences can be manually managed by turning off the automatic scheduling feature.
Iteration cadences automate iteration scheduling. You can use them to Iteration cadences automate iteration scheduling. You can use them to
automate creating iterations every 1, 2, 3, or 4 weeks. You can also automate creating iterations every 1, 2, 3, or 4 weeks. You can also
@ -206,8 +201,9 @@ To create an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group. 1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**. 1. On the left sidebar, select **Issues > Iterations**.
1. Select **New iteration cadence**. 1. Select **New iteration cadence**.
1. Complete the fields. 1. Enter the title and description of the iteration cadence.
- Enter the title and description of the iteration cadence. 1. To manually manage the iteration cadence, clear the **Enable automatic scheduling** checkbox and skip the next step.
1. Complete the required fields to use automatic scheduling.
- Select the automation start date of the iteration cadence. Iterations will be scheduled to - Select the automation start date of the iteration cadence. Iterations will be scheduled to
begin on the same day of the week as the day of the week of the start date. begin on the same day of the week as the day of the week of the start date.
- From the **Duration** dropdown list, select how many weeks each iteration should last. - From the **Duration** dropdown list, select how many weeks each iteration should last.
@ -228,7 +224,7 @@ To edit an iteration cadence:
1. On the left sidebar, select **Issues > Iterations**. 1. On the left sidebar, select **Issues > Iterations**.
1. Select **Edit iteration cadence**. 1. Select **Edit iteration cadence**.
When you edit the **Automation start date** field, When you are using automatic scheduling and edit the **Automation start date** field,
you must set a new start date that doesn't overlap with the existing you must set a new start date that doesn't overlap with the existing
current or past iterations. current or past iterations.
@ -236,52 +232,23 @@ Editing **Upcoming iterations** is a non-destructive action.
If ten upcoming iterations already exist, changing the number under **Upcoming iterations** to `2` If ten upcoming iterations already exist, changing the number under **Upcoming iterations** to `2`
doesn't delete the eight existing upcoming iterations. doesn't delete the eight existing upcoming iterations.
### Delete an iteration cadence #### Turn on automatic scheduling for manual iterations cadence
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
Prerequisites:
- You must have at least the Reporter role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group. 1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**. 1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete. 1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence for which you want to enable automatic scheduling.
1. Select **Delete cadence** in the confirmation modal. 1. Check the **Enable automatic scheduling** checkbox.
### Manual iteration cadences
When you **enable** the iteration cadences feature, all previously
created iterations are added to a default iteration cadence.
You can continue to add, edit, and remove iterations in
this default cadence.
#### Convert a manual cadence to use automatic scheduling
WARNING:
The upgrade is irreversible. After it's done, a new manual iteration cadence cannot be created.
Prerequisites:
- You must have created [iterations](#iterations) without cadences before enabling iteration cadences for your group.
To upgrade the iteration cadence to use the automation features:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade.
1. Complete the required fields **Duration**, **Upcoming iterations**, and **Automation start date**. 1. Complete the required fields **Duration**, **Upcoming iterations**, and **Automation start date**.
For **Automation start date**, you can select any date that doesn't overlap with the existing open iterations. For **Automation start date**, you can select any date that doesn't overlap with the existing open iterations.
If you have upcoming iterations, the automatic scheduling adjusts them appropriately to fit If you have upcoming iterations, the automatic scheduling adjusts them appropriately to fit
your chosen duration. your chosen duration.
1. Select **Save changes**. 1. Select **Save changes**.
#### Converted cadences example When you want to manage your iterations cadence manually again, edit your cadence and uncheck the **Enable automatic scheduling** checkbox.
For example, suppose it's Friday, April 15, and you have three iterations in a manual cadence: #### Example of turning on automatic scheduling for manual iterations cadence
Suppose it's Friday, April 15, and you have three iteration in a manual iterations cadence:
- Monday, April 4 - Friday, April 8 (closed) - Monday, April 4 - Friday, April 8 (closed)
- Tuesday, April 12 - Friday, April 15 (ongoing) - Tuesday, April 12 - Friday, April 15 (ongoing)
@ -305,3 +272,20 @@ is changed to "April 18 - Sunday, April 24".
An additional upcoming iteration "April 25 - Sunday, May 1" is scheduled An additional upcoming iteration "April 25 - Sunday, May 1" is scheduled
to satisfy the requirement that there are at least two upcoming iterations scheduled. to satisfy the requirement that there are at least two upcoming iterations scheduled.
### Delete an iteration cadence
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
Prerequisites:
- You must have at least the Reporter role for a group.
Deleting an iteration cadence also deletes all iterations within that cadence.
To delete an iteration cadence:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
1. Select **Delete cadence**.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -13,15 +13,56 @@ When you have a lot of issues, it can be hard to get an overview.
With weighted issues, you can get a better idea of how much time, With weighted issues, you can get a better idea of how much time,
value, or complexity a given issue has or costs. value, or complexity a given issue has or costs.
You can set the weight of an issue during its creation, by changing the ## View the issue weight
value in the dropdown menu. You can set it to a non-negative integer
value from 0, 1, 2, and so on.
You can remove weight from an issue as well.
A user with a Reporter role (or above) can set the weight.
This value appears on the right sidebar of an individual issue, as well as You can view the issue weight on:
in the issues page next to a weight icon (**{weight}**).
As an added bonus, you can see the total sum of all issues on the milestone page. - The right sidebar of each issue.
- The issues page, next to a weight icon (**{weight}**).
- [Issue boards](../issue_board.md), next to a weight icon (**{weight}**).
- The [milestone](../milestones/index.md) page, as a total sum of issue weights.
![issue page](img/issue_weight_v13_11.png) ## Set the issue weight
Prerequisites:
- You must have at least the Reporter role for the project.
You can set the issue weight when you create or edit an issue.
You must use whole numbers (like 0, 1, 2). Negative numbers or fractions are not accepted.
When you change the weight of an issue, the new value overwrites the previous value.
### When you create an issue
To set the issue weight when you [create an issue](managing_issues.md#create-an-issue), enter a
number under **Weight**.
### From an existing issue
To set the issue weight from an existing issue:
1. Go to the issue.
1. On the right sidebar, in the **Weight** section, select **Edit**.
1. Enter the new weight.
1. Select any area outside the dropdown list.
### From an issue board
To set the issue weight when you [edit an issue from an issue board](../issue_board.md#edit-an-issue):
1. Go to your issue board.
1. Select an issue card (not its title).
1. On the right sidebar, in the **Weight** section, select **Edit**.
1. Enter the new weight.
1. Select any area outside the dropdown list.
## Remove issue weight
Prerequisites:
- You must have at least the Reporter role for the project.
To remove the issue weight, follow the same steps as when you [set the issue weight](#set-the-issue-weight),
and select **remove weight**.

View File

@ -151,48 +151,6 @@ module Gitlab
model.logger = old_loggers[connection_name] model.logger = old_loggers[connection_name]
end end
end end
module Ci
class DailyBuildGroupReportResult
DEFAULT_BRANCH = 'master'
COUNT_OF_DAYS = 5
def initialize(project)
@project = project
@last_pipeline = project.last_pipeline
end
def seed
COUNT_OF_DAYS.times do |count|
date = Time.now.utc - count.day
create_report(date)
end
end
private
attr_reader :project, :last_pipeline
def create_report(date)
last_pipeline.builds.uniq(&:group_name).each do |build|
::Ci::DailyBuildGroupReportResult.create(
project: project,
last_pipeline: last_pipeline,
date: date,
ref_path: last_pipeline.source_ref_path,
group_name: build.group_name,
data: {
'coverage' => rand(20..99)
},
group: project.group,
default_branch: last_pipeline.default_branch?
)
rescue ActiveRecord::RecordNotUnique
return false
end
end
end
end
end end
end end
# :nocov: # :nocov:

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module Seeders
module Ci
class DailyBuildGroupReportResult
DEFAULT_BRANCH = 'master'
COUNT_OF_DAYS = 5
def initialize(project)
@project = project
@last_pipeline = project.last_pipeline
end
def seed
COUNT_OF_DAYS.times do |count|
date = Time.now.utc - count.day
create_report(date)
end
end
private
attr_reader :project, :last_pipeline
def create_report(date)
last_pipeline.builds.uniq(&:group_name).each do |build|
::Ci::DailyBuildGroupReportResult.create(
project: project,
last_pipeline: last_pipeline,
date: date,
ref_path: last_pipeline.source_ref_path,
group_name: build.group_name,
data: {
'coverage' => rand(20..99)
},
group: project.group,
default_branch: last_pipeline.default_branch?
)
rescue ActiveRecord::RecordNotUnique
return false
end
end
end
end
end
end

View File

@ -33,33 +33,50 @@ module Gitlab
@endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '') @endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '')
end end
def issue_spam?(spam_issue:, user:, context: {}) def spam?(spammable:, user:, context: {}, extra_features: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context) metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key }
protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features }
pb, grpc_method = build_protobuf(**protobuf_args)
response = grpc_method.call(pb, metadata: metadata)
response = grpc_client.check_for_spam_issue(issue,
metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict) verdict = convert_verdict_to_gitlab_constant(response.verdict)
[verdict, response.extra_attributes.to_h, response.error] [verdict, response.extra_attributes.to_h, response.error]
end end
private private
def get_spammable_mappings(spammable)
case spammable
when Issue
[::Spamcheck::Issue, grpc_client.method(:check_for_spam_issue)]
when Snippet
[::Spamcheck::Snippet, grpc_client.method(:check_for_spam_snippet)]
else
raise ArgumentError, "Not a spammable type: #{spammable.class.name}"
end
end
def convert_verdict_to_gitlab_constant(verdict) def convert_verdict_to_gitlab_constant(verdict)
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict) VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
end end
def build_issue_protobuf(issue:, user:, context:) def build_protobuf(spammable:, user:, context:, extra_features:)
issue_pb = ::Spamcheck::Issue.new protobuf_class, grpc_method = get_spammable_mappings(spammable)
issue_pb.title = issue.spam_title || '' pb = protobuf_class.new(**extra_features)
issue_pb.description = issue.spam_description || '' pb.title = spammable.spam_title || ''
issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at pb.description = spammable.spam_description || ''
issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
issue_pb.user_in_project = user.authorized_project?(issue.project) pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
issue_pb.project = build_project_protobuf(issue) pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action) pb.user = build_user_protobuf(user)
issue_pb.user = build_user_protobuf(user)
issue_pb unless spammable.project.nil?
pb.user_in_project = user.authorized_project?(spammable.project)
pb.project = build_project_protobuf(spammable)
end
[pb, grpc_method]
end end
def build_user_protobuf(user) def build_user_protobuf(user)

View File

@ -22084,9 +22084,6 @@ msgstr ""
msgid "Iterations|Cadence name" msgid "Iterations|Cadence name"
msgstr "" msgstr ""
msgid "Iterations|Can be converted"
msgstr ""
msgid "Iterations|Cancel" msgid "Iterations|Cancel"
msgstr "" msgstr ""
@ -22132,6 +22129,9 @@ msgstr ""
msgid "Iterations|Edit iteration cadence" msgid "Iterations|Edit iteration cadence"
msgstr "" msgstr ""
msgid "Iterations|Enable automatic scheduling"
msgstr ""
msgid "Iterations|Enable roll over" msgid "Iterations|Enable roll over"
msgstr "" msgstr ""
@ -22147,12 +22147,6 @@ msgstr ""
msgid "Iterations|Iterations are scheduled to start on %{weekday}s." msgid "Iterations|Iterations are scheduled to start on %{weekday}s."
msgstr "" msgstr ""
msgid "Iterations|Learn more about automatic scheduling"
msgstr ""
msgid "Iterations|Manual management of iterations will be deprecated in GitLab 15.6. Convert your manual cadence to use automated scheduling when you are ready."
msgstr ""
msgid "Iterations|Move incomplete issues to the next iteration." msgid "Iterations|Move incomplete issues to the next iteration."
msgstr "" msgstr ""
@ -22210,9 +22204,6 @@ msgstr ""
msgid "Iterations|The iteration has been deleted." msgid "Iterations|The iteration has been deleted."
msgstr "" msgstr ""
msgid "Iterations|This cadence can be converted to use automated scheduling"
msgstr ""
msgid "Iterations|This will delete the cadence as well as all of the iterations within it." msgid "Iterations|This will delete the cadence as well as all of the iterations within it."
msgstr "" msgstr ""
@ -22222,9 +22213,6 @@ msgstr ""
msgid "Iterations|Title" msgid "Iterations|Title"
msgstr "" msgstr ""
msgid "Iterations|To convert this cadence to automatic scheduling, add a duration and number of upcoming iterations. The upgrade is irreversible."
msgstr ""
msgid "Iterations|Unable to find iteration cadence." msgid "Iterations|Unable to find iteration cadence."
msgstr "" msgstr ""
@ -22237,9 +22225,6 @@ msgstr ""
msgid "Iterations|Upcoming iterations" msgid "Iterations|Upcoming iterations"
msgstr "" msgstr ""
msgid "Iterations|Your manual cadence can be converted to use automated scheduling"
msgstr ""
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group" msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
msgstr "" msgstr ""

View File

@ -217,6 +217,7 @@
"eslint-import-resolver-jest": "3.0.2", "eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.2", "eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-no-jquery": "2.7.0", "eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-unsanitized": "^4.0.1",
"gettext-extractor": "^3.5.3", "gettext-extractor": "^3.5.3",
"gettext-extractor-vue": "^5.0.0", "gettext-extractor-vue": "^5.0.0",
"glob": "^7.1.6", "glob": "^7.1.6",

View File

@ -77,44 +77,4 @@ RSpec.describe Gitlab::Seeder do
end end
end end
end end
describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
subject(:build_report) do
described_class.new(project)
end
describe '#seed' do
it 'creates daily build results for the project' do
expect { build_report.seed }.to change {
Ci::DailyBuildGroupReportResult.count
}.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'matches project data with last report' do
build_report.seed
report = project.daily_build_group_report_results.last
reports_count = project.daily_build_group_report_results.count
expect(build.group_name).to eq(report.group_name)
expect(pipeline.source_ref_path).to eq(report.ref_path)
expect(pipeline.default_branch?).to eq(report.default_branch)
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'does not raise error on RecordNotUnique' do
build_report.seed
build_report.seed
reports_count = project.daily_build_group_report_results.count
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
end
end
end end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
subject(:build_report) do
described_class.new(project)
end
describe '#seed' do
it 'creates daily build results for the project' do
expect { build_report.seed }.to change {
Ci::DailyBuildGroupReportResult.count
}.by(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'matches project data with last report' do
build_report.seed
report = project.daily_build_group_report_results.last
reports_count = project.daily_build_group_report_results.count
expect(build.group_name).to eq(report.group_name)
expect(pipeline.source_ref_path).to eq(report.ref_path)
expect(pipeline.default_branch?).to eq(report.default_branch)
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
it 'does not raise error on RecordNotUnique' do
build_report.seed
build_report.seed
reports_count = project.daily_build_group_report_results.count
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
end
end
end

View File

@ -17,6 +17,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
end end
let_it_be(:issue) { create(:issue, description: 'Test issue description') } let_it_be(:issue) { create(:issue, description: 'Test issue description') }
let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') }
let(:response) do let(:response) do
verdict = ::Spamcheck::SpamVerdict.new verdict = ::Spamcheck::SpamVerdict.new
@ -26,7 +27,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
verdict verdict
end end
subject { described_class.new.issue_spam?(spam_issue: issue, user: user) } subject { described_class.new.spam?(spammable: issue, user: user) }
before do before do
stub_application_setting(spam_check_endpoint_url: endpoint) stub_application_setting(spam_check_endpoint_url: endpoint)
@ -56,10 +57,11 @@ RSpec.describe Gitlab::Spamcheck::Client do
end end
end end
describe '#issue_spam?' do shared_examples 'check for spam' do
before do before do
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance| allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
allow(instance).to receive(:check_for_spam_issue).and_return(response) allow(instance).to receive(:check_for_spam_issue).and_return(response)
allow(instance).to receive(:check_for_spam_snippet).and_return(response)
end end
end end
@ -89,12 +91,26 @@ RSpec.describe Gitlab::Spamcheck::Client do
end end
end end
describe "#build_issue_protobuf", :aggregate_failures do describe "#spam?", :aggregate_failures do
it 'builds the expected protobuf object' do describe 'issue' do
subject { described_class.new.spam?(spammable: issue, user: user) }
it_behaves_like "check for spam"
end
describe 'snippet' do
subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) }
it_behaves_like "check for spam"
end
end
describe "#build_protobuf", :aggregate_failures do
it 'builds the expected issue protobuf object' do
cxt = { action: :create } cxt = { action: :create }
issue_pb = described_class.new.send(:build_issue_protobuf, issue_pb, _ = described_class.new.send(:build_protobuf,
issue: issue, user: user, spammable: issue, user: user,
context: cxt) context: cxt, extra_features: {})
expect(issue_pb.title).to eq issue.title expect(issue_pb.title).to eq issue.title
expect(issue_pb.description).to eq issue.description expect(issue_pb.description).to eq issue.description
expect(issue_pb.user_in_project).to be false expect(issue_pb.user_in_project).to be false
@ -104,6 +120,22 @@ RSpec.describe Gitlab::Spamcheck::Client do
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE) expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(issue_pb.user.username).to eq user.username expect(issue_pb.user.username).to eq user.username
end end
it 'builds the expected snippet protobuf object' do
cxt = { action: :create }
snippet_pb, _ = described_class.new.send(:build_protobuf,
spammable: snippet, user: user,
context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] })
expect(snippet_pb.title).to eq snippet.title
expect(snippet_pb.description).to eq snippet.description
expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at)
expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at)
expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(snippet_pb.user.username).to eq user.username
expect(snippet_pb.user.username).to eq user.username
expect(snippet_pb.files.first.path).to eq 'first.rb'
expect(snippet_pb.files.last.path).to eq 'second.rb'
end
end end
describe '#build_user_protobuf', :aggregate_failures do describe '#build_user_protobuf', :aggregate_failures do
@ -143,6 +175,19 @@ RSpec.describe Gitlab::Spamcheck::Client do
end end
end end
describe "#get_spammable_mappings", :aggregate_failures do
it 'is an expected spammable' do
protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue)
expect(protobuf_class).to eq ::Spamcheck::Issue
end
it 'is an unexpected spammable' do
expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error(
ArgumentError, 'Not a spammable type: String'
)
end
end
private private
def timestamp_to_protobuf_timestamp(timestamp) def timestamp_to_protobuf_timestamp(timestamp)

View File

@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance| allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true) allow(instance).to receive(:spam?).and_return(true)
end end
stub_feature_flags(allow_possible_spam: false)
project.add_developer(user) project.add_developer(user)
end end
@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do
allow_next_instance_of(Spam::AkismetService) do |instance| allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true) allow(instance).to receive(:spam?).and_return(true)
end end
stub_feature_flags(allow_possible_spam: false)
end end
context 'when the snippet is private' do context 'when the snippet is private' do

View File

@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance| allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true) allow(instance).to receive(:spam?).and_return(true)
end end
stub_feature_flags(allow_possible_spam: false)
end end
context 'when the snippet is private' do context 'when the snippet is private' do
@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
allow_next_instance_of(Spam::AkismetService) do |instance| allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true) allow(instance).to receive(:spam?).and_return(true)
end end
stub_feature_flags(allow_possible_spam: false)
end end
context 'when the snippet is private' do context 'when the snippet is private' do

View File

@ -18,7 +18,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
subject(:content_security_policy) { response.headers['Content-Security-Policy'] } subject(:content_security_policy) { response.headers['Content-Security-Policy'] }
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_ids') } it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_id') }
context 'with no self-managed instance configured' do context 'with no self-managed instance configured' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') } let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }

View File

@ -6,6 +6,8 @@ RSpec.describe Spam::SpamActionService do
include_context 'includes Spam constants' include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: author) } let(:issue) { create(:issue, project: project, author: author) }
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
let(:project_snippet) { create(:project_snippet, :public, author: author) }
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referer) { 'fake-http-referer' } let(:fake_referer) { 'fake-http-referer' }
@ -27,6 +29,7 @@ RSpec.describe Spam::SpamActionService do
before do before do
issue.spam = false issue.spam = false
personal_snippet.spam = false
end end
describe 'constructor argument validation' do describe 'constructor argument validation' do
@ -50,24 +53,24 @@ RSpec.describe Spam::SpamActionService do
end end
end end
shared_examples 'creates a spam log' do shared_examples 'creates a spam log' do |target_type|
it do it do
expect { subject } expect { subject }
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue') .to log_spam(title: target.title, description: target.description, noteable_type: target_type)
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above # TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
new_spam_log = SpamLog.last new_spam_log = SpamLog.last
expect(new_spam_log.user_id).to eq(user.id) expect(new_spam_log.user_id).to eq(user.id)
expect(new_spam_log.title).to eq(issue.title) expect(new_spam_log.title).to eq(target.title)
expect(new_spam_log.description).to eq(issue.description) expect(new_spam_log.description).to eq(target.spam_description)
expect(new_spam_log.source_ip).to eq(fake_ip) expect(new_spam_log.source_ip).to eq(fake_ip)
expect(new_spam_log.user_agent).to eq(fake_user_agent) expect(new_spam_log.user_agent).to eq(fake_user_agent)
expect(new_spam_log.noteable_type).to eq('Issue') expect(new_spam_log.noteable_type).to eq(target_type)
expect(new_spam_log.via_api).to eq(true) expect(new_spam_log.via_api).to eq(true)
end end
end end
describe '#execute' do shared_examples 'execute spam action service' do |target_type|
let(:fake_captcha_verification_service) { double(:captcha_verification_service) } let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) } let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false } let(:allowlisted) { false }
@ -82,20 +85,22 @@ RSpec.describe Spam::SpamActionService do
let(:verdict_service_args) do let(:verdict_service_args) do
{ {
target: issue, target: target,
user: user, user: user,
options: verdict_service_opts, options: verdict_service_opts,
context: { context: {
action: :create, action: :create,
target_type: 'Issue' target_type: target_type
} },
extra_features: extra_features
} }
end end
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) } let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do subject do
described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create) described_service = described_class.new(spammable: target, spam_params: spam_params, extra_features:
extra_features, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted) allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute described_service.execute
end end
@ -136,7 +141,7 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have not changed' do context 'when spammable attributes have not changed' do
before do before do
issue.closed_at = Time.zone.now allow(target).to receive(:has_changes_to_save?).and_return(true)
end end
it 'does not create a spam log' do it 'does not create a spam log' do
@ -146,11 +151,11 @@ RSpec.describe Spam::SpamActionService do
context 'when spammable attributes have changed' do context 'when spammable attributes have changed' do
let(:expected_service_check_response_message) do let(:expected_service_check_response_message) do
/Check Issue spammable model for any errors or CAPTCHA requirement/ /Check #{target_type} spammable model for any errors or CAPTCHA requirement/
end end
before do before do
issue.description = 'Lovely Spam! Wonderful Spam!' target.description = 'Lovely Spam! Wonderful Spam!'
end end
context 'when allowlisted' do context 'when allowlisted' do
@ -170,13 +175,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW) allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
end end
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do it 'marks as spam' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam expect(target).to be_spam
end end
end end
@ -185,13 +190,13 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER) allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
end end
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log', target_type
it 'marks as spam' do it 'marks as spam' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam expect(target).to be_spam
end end
end end
@ -200,20 +205,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW) allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
end end
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do it 'does not mark as spam' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam expect(target).not_to be_spam
end end
it 'marks as needing reCAPTCHA' do it 'marks as needing reCAPTCHA' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_needs_recaptcha expect(target).to be_needs_recaptcha
end end
end end
@ -222,20 +227,20 @@ RSpec.describe Spam::SpamActionService do
allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM) allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM)
end end
it_behaves_like 'creates a spam log' it_behaves_like 'creates a spam log', target_type
it 'does not mark as spam' do it 'does not mark as spam' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam expect(target).not_to be_spam
end end
it 'does not mark as needing CAPTCHA' do it 'does not mark as needing CAPTCHA' do
response = subject response = subject
expect(response.message).to match(expected_service_check_response_message) expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_needs_recaptcha expect(target).not_to be_needs_recaptcha
end end
end end
@ -249,7 +254,7 @@ RSpec.describe Spam::SpamActionService do
end end
it 'clears spam flags' do it 'clears spam flags' do
expect(issue).to receive(:clear_spam_flags!) expect(target).to receive(:clear_spam_flags!)
subject subject
end end
@ -265,7 +270,7 @@ RSpec.describe Spam::SpamActionService do
end end
it 'clears spam flags' do it 'clears spam flags' do
expect(issue).to receive(:clear_spam_flags!) expect(target).to receive(:clear_spam_flags!)
subject subject
end end
@ -285,4 +290,27 @@ RSpec.describe Spam::SpamActionService do
end end
end end
end end
describe '#execute' do
describe 'issue' do
let(:target) { issue }
let(:extra_features) { {} }
it_behaves_like 'execute spam action service', 'Issue'
end
describe 'project snippet' do
let(:target) { project_snippet }
let(:extra_features) { { files: [{ path: 'project.rb' }] } }
it_behaves_like 'execute spam action service', 'ProjectSnippet'
end
describe 'personal snippet' do
let(:target) { personal_snippet }
let(:extra_features) { { files: [{ path: 'personal.rb' }] } }
it_behaves_like 'execute spam action service', 'PersonalSnippet'
end
end
end end

View File

@ -17,9 +17,10 @@ RSpec.describe Spam::SpamVerdictService do
let(:check_for_spam) { true } let(:check_for_spam) { true }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) } let_it_be(:issue) { create(:issue, author: user) }
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
let(:service) do let(:service) do
described_class.new(user: user, target: issue, options: {}) described_class.new(user: user, target: target, options: {})
end end
let(:attribs) do let(:attribs) do
@ -31,7 +32,7 @@ RSpec.describe Spam::SpamVerdictService do
stub_feature_flags(allow_possible_spam: false) stub_feature_flags(allow_possible_spam: false)
end end
describe '#execute' do shared_examples 'execute spam verdict service' do
subject { service.execute } subject { service.execute }
before do before do
@ -172,7 +173,8 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
describe '#akismet_verdict' do shared_examples 'akismet verdict' do
let(:target) { issue }
subject { service.send(:akismet_verdict) } subject { service.send(:akismet_verdict) }
context 'if Akismet is enabled' do context 'if Akismet is enabled' do
@ -227,7 +229,7 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
describe '#spamcheck_verdict' do shared_examples 'spamcheck verdict' do
subject { service.send(:spamcheck_verdict) } subject { service.send(:spamcheck_verdict) }
context 'if a Spam Check endpoint enabled and set to a URL' do context 'if a Spam Check endpoint enabled and set to a URL' do
@ -254,7 +256,7 @@ RSpec.describe Spam::SpamVerdictService do
before do before do
allow(service).to receive(:spamcheck_client).and_return(spam_client) allow(service).to receive(:spamcheck_client).and_return(spam_client)
allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error]) allow(spam_client).to receive(:spam?).and_return([verdict, attribs, error])
end end
context 'if the result is a NOOP verdict' do context 'if the result is a NOOP verdict' do
@ -365,7 +367,7 @@ RSpec.describe Spam::SpamVerdictService do
let(:attribs) { nil } let(:attribs) { nil }
before do before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted) allow(spam_client).to receive(:spam?).and_raise(GRPC::Aborted)
end end
it 'returns nil' do it 'returns nil' do
@ -387,7 +389,7 @@ RSpec.describe Spam::SpamVerdictService do
let(:attribs) { nil } let(:attribs) { nil }
before do before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded) allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded)
end end
it 'returns nil' do it 'returns nil' do
@ -416,4 +418,46 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
end end
describe '#execute' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'execute spam verdict service'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'execute spam verdict service'
end
end
describe '#akismet_verdict' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'akismet verdict'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'akismet verdict'
end
end
describe '#spamcheck_verdict' do
describe 'issue' do
let(:target) { issue }
it_behaves_like 'spamcheck verdict'
end
describe 'snippet' do
let(:target) { snippet }
it_behaves_like 'spamcheck verdict'
end
end
end end

View File

@ -14,7 +14,8 @@ RSpec.shared_examples 'checking spam' do
spammable: kind_of(Snippet), spammable: kind_of(Snippet),
spam_params: spam_params, spam_params: spam_params,
user: an_instance_of(User), user: an_instance_of(User),
action: action action: action,
extra_features: { files: an_instance_of(Array) }
} }
) do |instance| ) do |instance|
expect(instance).to receive(:execute) expect(instance).to receive(:execute)

View File

@ -5192,6 +5192,11 @@ eslint-plugin-no-jquery@2.7.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72" resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72"
integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w== integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w==
eslint-plugin-no-unsanitized@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.1.tgz#e2343265467ba2270ade478cbe07bbafeaea412d"
integrity sha512-y/lAMWnPPC7RYuUdxlEL/XiCL8FehN9h9s3Kjqbp/Kv0i9NZs+IXSC2kS546Fa4Bumwy31HlVS/OdWX0Kxb5Xg==
eslint-plugin-promise@^4.2.1: eslint-plugin-promise@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"