Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-07 15:08:49 +00:00
parent 0254867cf0
commit a93bf027c2
130 changed files with 725 additions and 1013 deletions

View File

@ -118,8 +118,8 @@
- "{,ee/}spec/**/*.rb"
.db-patterns: &db-patterns
- "{,ee/}{db}/**/*"
- "{,ee}/spec/{db,migrations}/**/*"
- "{,ee/}{,spec/}{db,migrations}/**/*"
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration/**/*"
.backstage-patterns: &backstage-patterns
- "Dangerfile"

View File

@ -33,7 +33,7 @@ export default {
<template>
<a :href="branchHref" class="btn-link d-flex align-items-center">
<span class="d-flex append-right-default ide-search-list-current-icon">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
<icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>

View File

@ -12,7 +12,7 @@ export default {
<div v-if="!lastCommitMsg" class="multi-file-commit-panel-section ide-commit-empty-state">
<div class="ide-commit-empty-state-container">
<div class="svg-content svg-80"><img :src="noChangesStateSvgPath" /></div>
<div class="append-right-default gl-ml-3">
<div class="gl-mr-3 gl-ml-3">
<div class="text-content text-center">
<h4>{{ __('No changes') }}</h4>
<p>{{ __('Edit files in the editor and commit changes here') }}</p>

View File

@ -13,7 +13,7 @@ export default {
<div class="svg-content svg-80">
<img :src="committedStateSvgPath" :alt="s__('IDE|Successful commit')" />
</div>
<div class="append-right-default gl-ml-3">
<div class="gl-mr-3 gl-ml-3">
<div class="text-content text-center">
<h4>{{ __('All changes are committed') }}</h4>
<p v-html="lastCommitMsg"></p>

View File

@ -48,7 +48,7 @@ export default {
<template>
<div class="d-flex align-items-center ide-file-templates qa-file-templates-bar">
<strong class="append-right-default"> {{ __('File templates') }} </strong>
<strong class="gl-mr-3"> {{ __('File templates') }} </strong>
<dropdown
:data="templateTypes"
:label="selectedTemplateType.name || __('Choose a type...')"

View File

@ -26,7 +26,7 @@ export default {
<template>
<div class="ide-job-item">
<job-description :job="job" class="append-right-default" />
<job-description :job="job" class="gl-mr-3" />
<div class="ml-auto align-self-center">
<button v-if="job.started" type="button" class="btn btn-default btn-sm" @click="clickViewLog">
{{ __('View log') }}

View File

@ -40,7 +40,7 @@ export default {
<template>
<a :href="mergeRequestHref" class="btn-link d-flex align-items-center">
<span class="d-flex append-right-default ide-search-list-current-icon">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
<icon v-if="isActive" :size="18" name="mobile-issue-close" />
</span>
<span>

View File

@ -102,7 +102,7 @@ export default {
class="btn-link d-flex align-items-center"
@click.stop="setSearchType(searchType)"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
<icon :size="18" name="search" />
</span>
<span>{{ searchType.label }}</span>

View File

@ -8,9 +8,10 @@ import ModelManager from './common/model_manager';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
import { themes } from './themes';
import languages from './languages';
import schemas from './schemas';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
import { registerLanguages } from '../utils';
import { registerLanguages, registerSchemas } from '../utils';
function setupThemes() {
themes.forEach(theme => {
@ -44,6 +45,7 @@ export default class Editor {
setupThemes();
registerLanguages(...languages);
registerSchemas(...schemas);
this.debouncedUpdate = debounce(() => {
this.updateDimensions();

View File

@ -0,0 +1,4 @@
import json from './json';
import yaml from './yaml';
export default [json, yaml];

View File

@ -0,0 +1,8 @@
export default {
language: 'json',
options: {
validate: true,
enableSchemaRequest: true,
schemas: [],
},
};

View File

@ -0,0 +1,4 @@
export default {
uri: 'https://json.schemastore.org/gitlab-ci',
fileMatch: ['*.gitlab-ci.yml'],
};

View File

@ -0,0 +1,12 @@
import gitlabCi from './gitlab_ci';
export default {
language: 'yaml',
options: {
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [gitlabCi],
},
};

View File

@ -66,7 +66,7 @@ export const trimPathComponents = path =>
.join('/');
export function registerLanguages(def, ...defs) {
if (defs.length) defs.forEach(lang => registerLanguages(lang));
defs.forEach(lang => registerLanguages(lang));
const languageId = def.id;
@ -75,6 +75,19 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf);
}
export function registerSchemas({ language, options }, ...schemas) {
schemas.forEach(schema => registerSchemas(schema));
const defaults = {
json: languages.json.jsonDefaults,
yaml: languages.yaml.yamlDefaults,
};
if (defaults[language]) {
defaults[language].setDiagnosticsOptions(options);
}
}
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
export function trimTrailingWhitespace(content) {

View File

@ -1,5 +1,7 @@
<script>
import { GlAlert, GlLabel } from '@gitlab/ui';
import { last } from 'lodash';
import { n__ } from '~/locale';
import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
import {
calculateJiraImportLabel,
@ -47,6 +49,7 @@ export default {
};
},
update: ({ project }) => ({
importedIssuesCount: last(project.jiraImports.nodes)?.importedIssuesCount,
isInProgress: isInProgress(project.jiraImportStatus),
isFinished: isFinished(project.jiraImportStatus),
label: calculateJiraImportLabel(
@ -60,6 +63,13 @@ export default {
},
},
computed: {
finishedMessage() {
return n__(
'%d issue successfully imported with the label',
'%d issues successfully imported with the label',
this.jiraImport.importedIssuesCount,
);
},
labelTarget() {
return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`;
},
@ -87,7 +97,7 @@ export default {
{{ __('Import in progress. Refresh page to see newly added issues.') }}
</gl-alert>
<gl-alert v-if="shouldShowFinishedAlert" variant="success" @dismiss="hideFinishedAlert">
{{ __('Issues successfully imported with the label') }}
{{ finishedMessage }}
<gl-label
:background-color="jiraImport.label.color"
scoped

View File

@ -1,5 +1,3 @@
#import "~/jira_import/queries/jira_import.fragment.graphql"
query($fullPath: ID!) {
project(fullPath: $fullPath) {
issues {
@ -15,7 +13,8 @@ query($fullPath: ID!) {
jiraImportStatus
jiraImports {
nodes {
...JiraImport
importedIssuesCount
jiraProjectKey
}
}
}

View File

@ -81,7 +81,7 @@ export default {
v-if="shouldShowDeleteButton"
:class="{ disabled: deleteLoading }"
:disabled="deleteLoading"
class="btn btn-danger float-right append-right-default qa-delete-button"
class="btn btn-danger float-right gl-mr-3 qa-delete-button"
type="button"
@click="deleteIssuable"
>

View File

@ -72,7 +72,7 @@ export default {
<gl-deprecated-button
v-if="showBack"
size="sm"
class="append-right-default js-back-button"
class="gl-mr-3 js-back-button"
@click="onBackClick"
>
<icon name="angle-left" />

View File

@ -52,7 +52,7 @@ export default {
v-if="showReportSectionStatusIcon"
:status="status"
:status-icon-size="statusIconSize"
class="append-right-default"
class="gl-mr-3"
/>
<component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" />

View File

@ -45,7 +45,7 @@ export default {
</script>
<template>
<div class="report-block-list-issue report-block-list-issue-parent align-items-center">
<div class="report-block-list-icon append-right-default">
<div class="report-block-list-icon gl-mr-3">
<gl-loading-icon
v-if="statusIcon === 'loading'"
css-class="report-block-list-loading-icon"

View File

@ -59,7 +59,7 @@ export default {
<template v-else>
<button
class="btn-blank btn s32 square append-right-default"
class="btn-blank btn s32 square gl-mr-3"
type="button"
:aria-label="ariaLabel"
:disabled="isLoading"

View File

@ -46,7 +46,7 @@ export default {
<div>
<div class="mr-widget-body gl-display-flex">
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-mr-3 gl-align-self-start gl-mt-1"
>
<gl-icon :name="iconName" :size="24" />
</span>

View File

@ -13,7 +13,7 @@ export default {
</script>
<template>
<div class="circle-icon-container append-right-default align-self-start align-self-lg-center">
<div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
<icon :name="name" :size="24" />
</div>
</template>

View File

@ -129,7 +129,7 @@ export default {
<div class="media-body gl-ml-3" v-html="errorText"></div>
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="align-self-start append-right-default">
<a :href="status.details_path" class="align-self-start gl-mr-3">
<ci-icon :status="status" :size="24" :borderless="true" class="add-border" />
</a>
<div class="ci-widget-container d-flex">

View File

@ -33,7 +33,7 @@ export default {
</script>
<template>
<div class="d-flex align-self-start">
<div class="square s24 h-auto d-flex-center append-right-default">
<div class="square s24 h-auto d-flex-center gl-mr-3">
<div v-if="isLoading" class="mr-widget-icon d-inline-flex">
<gl-loading-icon size="md" class="mr-loading-icon d-inline-flex" />
</div>

View File

@ -83,7 +83,7 @@ export default {
<gl-deprecated-button
:aria-label="ariaLabel"
variant="blank"
class="commit-edit-toggle square s24 append-right-default"
class="commit-edit-toggle square s24 gl-mr-3"
@click.stop="toggle()"
>
<icon :name="collapseIcon" :size="16" />

View File

@ -1,6 +1,7 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { __ } from '~/locale';
export default {
components: {
@ -25,12 +26,22 @@ export default {
default: false,
},
},
computed: {
tooltipTitle() {
return this.isDisabled ? __('Required in this project.') : false;
},
},
};
</script>
<template>
<div class="inline">
<label :class="{ 'gl-text-gray-600': isDisabled }" data-testid="squashLabel">
<label
v-tooltip
:class="{ 'gl-text-gray-600': isDisabled }"
data-testid="squashLabel"
:data-title="tooltipTitle"
>
<input
:checked="value"
:disabled="isDisabled"

View File

@ -58,7 +58,7 @@ export default {
<template>
<div class="gl-display-flex">
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-mr-3 gl-align-self-start gl-mt-1"
>
<gl-icon :name="iconType" :size="18" data-testid="change-type-icon" />
</span>

View File

@ -261,7 +261,7 @@ export default {
</li>
</template>
<li v-else class="dropdown-menu-empty-item">
<div class="append-right-default gl-ml-3 gl-mt-3 gl-mb-3">
<div class="gl-mr-3 gl-ml-3 gl-mt-3 gl-mb-3">
<template v-if="loading">
{{ __('Loading...') }}
</template>

View File

@ -53,14 +53,9 @@ export default {
},
tooltipMessage() {
return this.canApply
? __('This also resolves the discussion')
? __('This also resolves this thread')
: __("Can't apply as this line has changed or the suggestion already matches its content.");
},
tooltipMessageBatch() {
return !this.canBeBatched
? __("Suggestions that change line count can't be added to batches, yet.")
: this.tooltipMessage;
},
isDisableButton() {
return this.isApplying || !this.canApply;
},
@ -129,15 +124,14 @@ export default {
</gl-deprecated-button>
</div>
<div v-else class="d-flex align-items-center">
<span v-if="canBeBatched" v-gl-tooltip.viewport="tooltipMessageBatch" tabindex="0">
<gl-deprecated-button
class="btn-inverted js-add-to-batch-btn btn-grouped"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
</gl-deprecated-button>
</span>
<gl-deprecated-button
v-if="canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
</gl-deprecated-button>
<span v-gl-tooltip.viewport="tooltipMessage" tabindex="0">
<gl-deprecated-button
class="btn-inverted js-apply-btn btn-grouped"

View File

@ -1,10 +1,11 @@
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierText from './renderers/render_identifier_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
const listRenderers = [renderKramdownList];
const textRenderers = [renderKramdownText, renderIdentifierText, renderEmbeddedRubyText];
const paragraphRenderers = [renderIdentifierParagraph];
const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
const executeRenderer = (renderers, node, context) => {
const availableRenderer = renderers.find(renderer => renderer.canRender(node, context));
@ -22,13 +23,18 @@ const buildCustomRendererFunctions = (customRenderers, defaults) => {
return Object.fromEntries(customEntries);
};
const buildCustomHTMLRenderer = (customRenderers = { list: [], text: [] }) => {
const buildCustomHTMLRenderer = (customRenderers = { list: [], paragraph: [], text: [] }) => {
const defaults = {
list(node, context) {
const allListRenderers = [...customRenderers.list, ...listRenderers];
return executeRenderer(allListRenderers, node, context);
},
paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context);
},
text(node, context) {
const allTextRenderers = [...customRenderers.text, ...textRenderers];

View File

@ -0,0 +1,16 @@
import { buildUneditableOpenTokens, buildUneditableCloseToken } from './build_uneditable_token';
const identifierRegex = /(^\[.+\]: .+)/;
const isIdentifier = text => {
return identifierRegex.test(text);
};
const canRender = (node, context) => {
return isIdentifier(context.getChildrenText(node));
};
const render = (_, { entering, origin }) =>
entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
export default { canRender, render };

View File

@ -1,81 +0,0 @@
import {
buildUneditableOpenTokens,
buildUneditableCloseTokens,
buildUneditableTokens,
} from './build_uneditable_token';
const identifierRegex = /(^\[.+\]: .+)/;
const isBasicIdentifier = ({ literal }) => {
return identifierRegex.test(literal);
};
const isInlineCodeNode = ({ type, tickCount }) => type === 'code' && tickCount === 1;
const hasAdjacentInlineCode = (isForward, node) => {
const direction = isForward ? 'next' : 'prev';
let currentNode = node;
while (currentNode[direction] && currentNode.literal !== null) {
if (isInlineCodeNode(currentNode)) {
return true;
}
currentNode = currentNode[direction];
}
return false;
};
const hasEnteringPotential = literal => literal.includes('[');
const hasExitingPotential = literal => literal.includes(']: ');
const hasAdjacentExit = node => {
let currentNode = node;
while (currentNode && currentNode.literal !== null) {
if (hasExitingPotential(currentNode.literal)) {
return true;
}
currentNode = currentNode.next;
}
return false;
};
const isEnteringWithAdjacentInlineCode = ({ literal, next }) => {
if (next && hasEnteringPotential(literal) && !hasExitingPotential(literal)) {
return hasAdjacentInlineCode(true, next) && hasAdjacentExit(next);
}
return false;
};
const isExitingWithAdjacentInlineCode = ({ literal, prev }) => {
if (prev && !hasEnteringPotential(literal) && hasExitingPotential(literal)) {
return hasAdjacentInlineCode(false, prev);
}
return false;
};
const isAdjacentInlineCodeIdentifier = node => {
return isEnteringWithAdjacentInlineCode(node) || isExitingWithAdjacentInlineCode(node);
};
const canRender = (node, context) => {
return isBasicIdentifier(node) || isAdjacentInlineCodeIdentifier(node, context);
};
const render = (node, { origin }) => {
if (isEnteringWithAdjacentInlineCode(node)) {
return buildUneditableOpenTokens(origin());
} else if (isExitingWithAdjacentInlineCode(node)) {
return buildUneditableCloseTokens(origin());
}
return buildUneditableTokens(origin());
};
export default { canRender, render };

View File

@ -408,7 +408,6 @@ img.emoji {
.append-right-5 { margin-right: 5px; }
.append-right-10 { margin-right: 10px; }
.append-right-15 { margin-right: 15px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-20 { margin-bottom: 20px; }

View File

@ -321,7 +321,13 @@ module ApplicationSettingsHelper
:email_restrictions_enabled,
:email_restrictions,
:issues_create_limit,
:raw_blob_request_limit
:raw_blob_request_limit,
:project_import_limit,
:project_export_limit,
:project_download_export_limit,
:group_import_limit,
:group_export_limit,
:group_download_export_limit
]
end

View File

@ -158,7 +158,13 @@ module ApplicationSettingImplementation
snowplow_iglu_registry_url: nil,
custom_http_clone_url_root: nil,
productivity_analytics_start_date: Time.current,
snippet_size_limit: 50.megabytes
snippet_size_limit: 50.megabytes,
project_import_limit: 6,
project_export_limit: 6,
project_download_export_limit: 1,
group_import_limit: 6,
group_export_limit: 6,
group_download_export_limit: 1
}
end

View File

@ -75,7 +75,12 @@ class ProjectStatistics < ApplicationRecord
end
def update_storage_size
self.storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size + snippets_size
storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size
# The `snippets_size` column was added on 20200622095419 but db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
# might try to update project statistics before the `snippets_size` column has been created.
storage_size += snippets_size if self.class.column_names.include?('snippets_size')
self.storage_size = storage_size
end
# Since this incremental update method does not call update_storage_size above,

View File

@ -0,0 +1,34 @@
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :project_import_limit, _('Max Project Import requests per minute per user'), class: 'label-bold'
= f.number_field :project_import_limit, class: 'form-control'
%fieldset
.form-group
= f.label :project_export_limit, _('Max Project Export requests per minute per user'), class: 'label-bold'
= f.number_field :project_export_limit, class: 'form-control'
%fieldset
.form-group
= f.label :project_download_export_limit, _('Max Project Export Download requests per minute per user'), class: 'label-bold'
= f.number_field :project_download_export_limit, class: 'form-control'
%fieldset
.form-group
= f.label :group_import_limit, _('Max Group Import requests per minute per user'), class: 'label-bold'
= f.number_field :group_import_limit, class: 'form-control'
%fieldset
.form-group
= f.label :group_export_limit, _('Max Group Export requests per minute per user'), class: 'label-bold'
= f.number_field :group_export_limit, class: 'form-control'
%fieldset
.form-group
= f.label :group_download_export_limit, _('Max Group Export Download requests per minute per user'), class: 'label-bold'
= f.number_field :group_download_export_limit, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' }

View File

@ -57,4 +57,15 @@
.settings-content
= render 'issue_limits'
%section.settings.as-import-export-limits.no-animate#js-import-export-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Import/Export Rate Limits')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure limits for Project/Group Import/Export.')
.settings-content
= render 'import_export_limits'
= render_if_exists 'admin/application_settings/ee_network_settings'

View File

@ -42,7 +42,7 @@
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), target: '_blank', rel: 'noopener noreferrer'
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0
.append-right-default
.gl-mr-3
= s_("CiVariable|Protected")
= render "shared/buttons/project_feature_toggle", is_checked: is_protected, label: s_("CiVariable|Toggle protected") do
%input{ type: "hidden",
@ -51,7 +51,7 @@
value: is_protected,
data: { default: is_protected_default.to_s } }
.ci-variable-body-item.ci-variable-masked-item.table-section.section-20.mr-0.border-top-0
.append-right-default
.gl-mr-3
= s_("CiVariable|Masked")
= render "shared/buttons/project_feature_toggle", is_checked: is_masked, label: s_("CiVariable|Toggle masked"), class_list: "js-project-feature-toggle project-feature-toggle qa-variable-masked" do
%input{ type: "hidden",

View File

@ -25,7 +25,7 @@
.nav-controls
- if @todos.any?(&:pending?)
.append-right-default
.gl-mr-3
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading d-flex align-items-center js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
%span.spinner.ml-1

View File

@ -5,7 +5,7 @@
.group-home-panel
.row.mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-mr-3.float-none
= group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline

View File

@ -6,7 +6,7 @@
.project-home-panel.js-show-on-project-root{ class: [("empty-project" if empty_repo)] }
.row.gl-mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-mr-3.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline

View File

@ -2,7 +2,7 @@
- can_create_project = current_user.can?(:create_projects, namespace)
- if forked_project = namespace.find_fork_of(@project)
.bordered-box.fork-thumbnail.text-center.gl-ml-3.append-right-default.gl-mt-3.gl-mb-3.forked
.bordered-box.fork-thumbnail.text-center.gl-ml-3.gl-mr-3.gl-mt-3.gl-mb-3.forked
= link_to project_path(forked_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
= group_icon(namespace, class: "avatar rect-avatar s100 identicon mx-auto")
@ -12,7 +12,7 @@
%h5.gl-mt-3
= namespace.human_name
- else
.bordered-box.fork-thumbnail.text-center.gl-ml-3.append-right-default.gl-mt-3.gl-mb-3{ class: ("disabled" unless can_create_project) }
.bordered-box.fork-thumbnail.text-center.gl-ml-3.gl-mr-3.gl-mt-3.gl-mb-3{ class: ("disabled" unless can_create_project) }
= link_to project_forks_path(@project, namespace_key: namespace.id),
method: "POST",
class: ("disabled has-tooltip" unless can_create_project),

View File

@ -9,7 +9,7 @@
.col-lg-9
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
%h5.gl-mt-0.gl-mb-0.gl-ml-3.append-right-default
%h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3
= _("Select a namespace to fork the project")
- @namespaces.each do |namespace|
= render 'fork_button', namespace: namespace

View File

@ -58,10 +58,10 @@
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project
- if show_last_commit_as_description
.description.d-none.d-sm-block.append-right-default
.description.d-none.d-sm-block.gl-mr-3
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present?
.description.d-none.d-sm-block.append-right-default
.description.d-none.d-sm-block.gl-mr-3
= markdown_field(project, :description)
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }

View File

@ -0,0 +1,5 @@
---
title: Add support for linting based on schemas in WebIDE
merge_request: 35838
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add temporary storage increase column
merge_request: 36107
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add count to imported Jira issues message
merge_request: 36075
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Convert Import/Export rate limits to configurable application settings
merge_request: 35728
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Reduce number of scanned commits for code intelligence
merge_request: 36093
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: FindRemoteRepository is storage scoped
merge_request: 35962
author:
type: added

View File

@ -501,11 +501,6 @@ production: &base
geo_registry_sync_worker:
cron: "*/1 * * * *"
# GitLab Geo migrated local files clean up worker
# NOTE: This will only take effect if Geo is enabled (secondary nodes only)
geo_migrated_local_files_clean_up_worker:
cron: "15 */6 * * *"
# Export pseudonymized data in CSV format for analysis
pseudonymizer_worker:
cron: "0 * * * *"
@ -514,7 +509,7 @@ production: &base
# NOTE: This will only take effect if elasticsearch is enabled.
elastic_index_bulk_cron_worker:
cron: "*/1 * * * *"
# Elasticsearch bulk updater for initial updates.
# NOTE: This will only take effect if elasticsearch is enabled.
elastic_index_initial_bulk_cron_worker:

View File

@ -522,9 +522,6 @@ Gitlab.ee do
Settings.cron_jobs['geo_metrics_update_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_metrics_update_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['geo_metrics_update_worker']['job_class'] ||= 'Geo::MetricsUpdateWorker'
Settings.cron_jobs['geo_migrated_local_files_clean_up_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_migrated_local_files_clean_up_worker']['cron'] ||= '15 */6 * * *'
Settings.cron_jobs['geo_migrated_local_files_clean_up_worker']['job_class'] ||= 'Geo::MigratedLocalFilesCleanUpWorker'
Settings.cron_jobs['geo_prune_event_log_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_prune_event_log_worker']['cron'] ||= '*/5 * * * *'
Settings.cron_jobs['geo_prune_event_log_worker']['job_class'] ||= 'Geo::PruneEventLogWorker'

View File

@ -0,0 +1,17 @@
const { languagesArr } = require('monaco-editor-webpack-plugin/out/languages');
// monaco-yaml library doesn't play so well with monaco-editor-webpack-plugin
// so the only way to include its workers is by patching the list of languages
// in monaco-editor-webpack-plugin and adding support for yaml workers. This is
// a known issue in the library and this workaround was suggested here:
// https://github.com/pengx17/monaco-yaml/issues/20
const yamlLang = languagesArr.find(t => t.label === 'yaml');
yamlLang.entry = [yamlLang.entry, '../../monaco-yaml/esm/monaco.contribution'];
yamlLang.worker = {
id: 'vs/language/yaml/yamlWorker',
entry: '../../monaco-yaml/esm/yaml.worker.js',
};
module.exports = require('monaco-editor-webpack-plugin');

View File

@ -5,7 +5,7 @@ const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CompressionPlugin = require('compression-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const vendorDllHash = require('./helpers/vendor_dll_hash');
@ -241,7 +241,7 @@ module.exports = {
},
{
test: /\.(eot|ttf|woff|woff2)$/,
include: /node_modules\/katex\/dist\/fonts/,
include: /node_modules\/(katex\/dist\/fonts|monaco-editor)/,
loader: 'file-loader',
options: {
name: '[name].[contenthash:8].[ext]',

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
class StealEncryptRunnersTokens < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
# This cleans after `EncryptRunnersTokens`
DOWNTIME = false
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('EncryptRunnersTokens')
end
def down
# no-op
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddImportExportLimitsToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :application_settings, :project_import_limit, :integer, default: 6, null: false
add_column :application_settings, :project_export_limit, :integer, default: 6, null: false
add_column :application_settings, :project_download_export_limit, :integer, default: 1, null: false
add_column :application_settings, :group_import_limit, :integer, default: 6, null: false
add_column :application_settings, :group_export_limit, :integer, default: 6, null: false
add_column :application_settings, :group_download_export_limit, :integer, default: 1, null: false
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddTemporaryStorageIncreaseToNamespaceLimits < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :namespace_limits, :temporary_storage_increase_ends_on, :date, null: true
end
end

View File

@ -9145,6 +9145,12 @@ CREATE TABLE public.application_settings (
compliance_frameworks smallint[] DEFAULT '{}'::smallint[] NOT NULL,
notify_on_unknown_sign_in boolean DEFAULT true NOT NULL,
default_branch_name text,
project_import_limit integer DEFAULT 6 NOT NULL,
project_export_limit integer DEFAULT 6 NOT NULL,
project_download_export_limit integer DEFAULT 1 NOT NULL,
group_import_limit integer DEFAULT 6 NOT NULL,
group_export_limit integer DEFAULT 6 NOT NULL,
group_download_export_limit integer DEFAULT 1 NOT NULL,
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
@ -13014,7 +13020,8 @@ CREATE TABLE public.namespace_aggregation_schedules (
CREATE TABLE public.namespace_limits (
additional_purchased_storage_size bigint DEFAULT 0 NOT NULL,
additional_purchased_storage_ends_on date,
namespace_id integer NOT NULL
namespace_id integer NOT NULL,
temporary_storage_increase_ends_on date
);
CREATE TABLE public.namespace_root_storage_statistics (
@ -22523,7 +22530,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20190220150130
20190222051615
20190225152525
20190225160300
20190225160301
20190228192410
20190301081611
@ -23576,9 +23582,11 @@ COPY "schema_migrations" (version) FROM STDIN;
20200626060151
20200626130220
20200630110826
20200701093859
20200702123805
20200703154822
20200704143633
20200706005325
20200706170536
\.

View File

@ -261,8 +261,8 @@ instance (`cache`, `shared_state` etc.).
| Metric | Type | Since | Description |
|:--------------------------------- |:------- |:----- |:----------- |
| `redis_client_exceptions_total` | Counter | 13.2 | Number of Redis client exceptions, broken down by exception class |
| `redis_client_requests_total` | Counter | 13.2 | Number of Redis client requests |
| `gitlab_redis_client_exceptions_total` | Counter | 13.2 | Number of Redis client exceptions, broken down by exception class |
| `gitlab_redis_client_requests_total` | Counter | 13.2 | Number of Redis client requests |
## Metrics shared directory

View File

@ -667,8 +667,8 @@ Parameters:
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group ID for creating nested group. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group. |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
### Options for `default_branch_protection`
@ -741,8 +741,8 @@ PUT /groups/:id
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group. |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
NOTE: **Note:**
The `projects` and `shared_projects` attributes in the response are deprecated and will be [removed in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).

View File

@ -384,7 +384,7 @@ Parameters:
| `email` | Yes | Email |
| `extern_uid` | No | External UID |
| `external` | No | Flags the user as external - true or false (default) |
| `extra_shared_runners_minutes_limit` | No | Extra pipeline minutes quota for this user **(STARTER)** |
| `extra_shared_runners_minutes_limit` | No | Extra pipeline minutes quota for this user (purchased in addition to the minutes included in the plan) **(STARTER)** |
| `force_random_password` | No | Set user password to a random value - true or false (default) |
| `group_id_for_saml` | No | ID of group where SAML has been configured |
| `linkedin` | No | LinkedIn |
@ -398,7 +398,7 @@ Parameters:
| `provider` | No | External provider name |
| `public_email` | No | The public email of the user |
| `reset_password` | No | Send user password reset link - true or false(default) |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user **(STARTER)** |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` **(STARTER)** |
| `skip_confirmation` | No | Skip confirmation - true or false (default) |
| `skype` | No | Skype ID |
| `theme_id` | No | The GitLab theme for the user (see [the user preference docs](../user/profile/preferences.md#navigation-theme) for more information) |
@ -426,7 +426,7 @@ Parameters:
| `email` | No | Email |
| `extern_uid` | No | External UID |
| `external` | No | Flags the user as external - true or false (default) |
| `extra_shared_runners_minutes_limit` | No | Extra pipeline minutes quota for this user **(STARTER)** |
| `extra_shared_runners_minutes_limit` | No | Extra pipeline minutes quota for this user (purchased in addition to the minutes included in the plan) **(STARTER)** |
| `group_id_for_saml` | No | ID of group where SAML has been configured |
| `id` | Yes | The ID of the user |
| `linkedin` | No | LinkedIn |
@ -439,7 +439,7 @@ Parameters:
| `projects_limit` | No | Limit projects each user can create |
| `provider` | No | External provider name |
| `public_email` | No | The public email of the user |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user **(STARTER)** |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` **(STARTER)** |
| `skip_reconfirmation` | No | Skip reconfirmation - true or false (default) |
| `skype` | No | Skype ID |
| `theme_id` | No | The GitLab theme for the user (see [the user preference docs](../user/profile/preferences.md#navigation-theme) for more information) |

View File

@ -580,13 +580,14 @@ Make sure the IdP provides a claim containing the user's email address, using cl
If after signing in into your SAML server you are redirected back to the sign in page and
no error is displayed, check your `production.log` file. It will most likely contain the
message `Can't verify CSRF token authenticity`. This means that there is an error during
the SAML request, but this error never reaches GitLab due to the CSRF check.
the SAML request, but in GitLab 11.7 and earlier this error never reaches GitLab due to
the CSRF check.
To bypass this you can add `skip_before_action :verify_authenticity_token` to the
`omniauth_callbacks_controller.rb` file immediately after the `class` line and
comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This
will allow the error to hit GitLab, where it can then be seen in the usual logs,
or as a flash message on the login screen.
comment out the `protect_from_forgery` line using a `#`. Restart Unicorn for this
change to take effect. This will allow the error to hit GitLab, where it can then
be seen in the usual logs, or as a flash message on the login screen.
That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for

View File

@ -19,8 +19,8 @@ GitLab uses [Semantic Versioning](https://semver.org/) for its releases:
For example, for GitLab version 12.10.6:
- `12` represents the major version. The major release was 12.0.0, but often referred to as 12.0.
- `10` represents the minor version. The minor release was 12.10.0, but often referred to as 12.10.
- `12` represents the major version. The major release was 12.0.0 but often referred to as 12.0.
- `10` represents the minor version. The minor release was 12.10.0 but often referred to as 12.10.
- `6` represents the patch number.
Any part of the version number can increment into multiple digits, for example, 13.10.11.
@ -37,7 +37,7 @@ The following table describes the version types and their release cadence:
We encourage everyone to run the [latest stable release](https://about.gitlab.com/releases/categories/releases/)
to ensure that you can easily upgrade to the most secure and feature-rich GitLab experience.
In order to make sure you can easily run the most recent stable release, we are working
To make sure you can easily run the most recent stable release, we are working
hard to keep the update process simple and reliable.
If you are unable to follow our monthly release cycle, there are a couple of
@ -79,7 +79,7 @@ We cannot guarantee that upgrading between major versions will be seamless.
We suggest upgrading to the latest available *minor* version within
your major version before proceeding to the next major version.
Doing this will address any backward-incompatible changes or deprecations
to help ensure a successful upgrade to next major release.
to help ensure a successful upgrade to the next major release.
It's also important to ensure that any background migrations have been fully completed
before upgrading to a new major version. To see the current size of the `background_migration` queue,
@ -110,14 +110,13 @@ Please see the table below for some examples:
| `13.2.0` | `11.5.0` | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.0` -> `13.2.0` | Four intermediate versions are required: the final `11.11`, `12.0`, and `12.10` releases, plus `13.0`. |
| `13.0.1` | `11.10.8` | `11.10.5` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.1` | Three intermediate versions are required: `11.11`, `12.0`, and `12.10`. |
| `12.10.6` | `11.3.4` | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.10.6` | Two intermediate versions are required: `11.11` and `12.0` |
| `12.9.5.` | `10.4.5` | `10.4.5` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.9.5` | Three intermediate versions are required: `10.8`, `11.11`, and `12.0`, then `12.9.5` |
| `12.9.5` | `10.4.5` | `10.4.5` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.9.5` | Three intermediate versions are required: `10.8`, `11.11`, and `12.0`, then `12.9.5` |
| `12.2.5` | `9.2.6` | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.2.5` | Four intermediate versions are required: `9.5`, `10.8`, `11.11`, `12.0`, then `12.2`. |
| `11.3.4` | `8.13.4` | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version 8, `9.5.10` is the last version in version 9, `10.8.7` is the last version in version 10. |
### Upgrades from versions earlier than 8.12
- `8.11.x` and earlier: you might have to upgrade to `8.12.0` specifically before you can
upgrade to `8.17.7`. This was [reported in an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/207259).
- `8.11.x` and earlier: you might have to upgrade to `8.12.0` specifically before you can upgrade to `8.17.7`. This was [reported in an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/207259).
- [CI changes prior to version 8.0](https://docs.gitlab.com/omnibus/update/README.html#updating-gitlab-ci-from-prior-540-to-version-714-via-omnibus-gitlab)
when it was merged into GitLab.
@ -155,11 +154,11 @@ and support cost.
1. Supporting parallel version discourages incremental upgrades which over time accumulate in
complexity and create upgrade challenges for all users. GitLab has a dedicated team ensuring that
incremental upgrades (and installations) are as simple as possible.
1. The number of changes created in the GitLab application is high, which contributes to backporting complexity to older releases. In number of cases, backporting has to go through the same
1. The number of changes created in the GitLab application is high, which contributes to backporting complexity to older releases. In several cases, backporting has to go through the same
review process a new change goes through.
1. Ensuring that tests pass on older release is a considerable challenge in some cases, and as such is very time consuming.
1. Ensuring that tests pass on the older release is a considerable challenge in some cases, and as such is very time-consuming.
Including new features in patch releases is not possible as that would break [Semantic Versioning](https://semver.org/).
Including new features in a patch release is not possible as that would break [Semantic Versioning](https://semver.org/).
Breaking [Semantic Versioning](https://semver.org/) has the following consequences for users that
have to adhere to various internal requirements (for example, org. compliance, verifying new features, and similar):
@ -169,7 +168,7 @@ have to adhere to various internal requirements (for example, org. compliance, v
In cases where a strategic user has a requirement to test a feature before it is
officially released, we can offer to create a Release Candidate (RC) version that will
include the specific feature. This should be needed only in extreme cases, and can be requested for
include the specific feature. This should be needed only in extreme cases and can be requested for
consideration by raising an issue in the [release/tasks](https://gitlab.com/gitlab-org/release/tasks/-/issues/new?issuable_template=Backporting-request) issue tracker.
It is important to note that the Release Candidate will also contain other features and changes as
it is not possible to easily isolate a specific feature (similar reasons as noted above). The
@ -179,7 +178,7 @@ accessible.
### Backporting to older releases
Backporting to more than one stable release is reserved for [security releases](#security-releases).
In some cases however, we may need to backport *a bug fix* to more than one stable
In some cases, however, we may need to backport *a bug fix* to more than one stable
release, depending on the severity of the bug.
The decision on whether backporting a change will be performed is done at the discretion of the
@ -220,12 +219,11 @@ This decision is made on a case-by-case basis.
Check [our release posts](https://about.gitlab.com/releases/categories/releases/).
Each month, we publish either a major or minor release of GitLab. At the end
of those release posts there are three sections to look for: Deprecations, Removals, and Important notes on upgrading. These will include:
of those release posts, there are three sections to look for: Deprecations, Removals, and Important notes on upgrading. These will include:
- Steps you need to perform as part of an upgrade.
For example [8.12](https://about.gitlab.com/releases/2016/09/22/gitlab-8-12-released/#upgrade-barometer)
required the Elasticsearch index to be recreated. Any older version of GitLab upgrading to 8.12 or higher
would require this.
required the Elasticsearch index to be recreated. Any older version of GitLab upgrading to 8.12 or higher would require this.
- Changes to the versions of software we support such as
[ceasing support for IE11 in GitLab 13](https://about.gitlab.com/releases/2020/03/22/gitlab-12-9-released/#ending-support-for-internet-explorer-11).

View File

@ -26,6 +26,7 @@ similarly mitigated by a rate limit.
- [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md).
- [Raw endpoints rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md).
- [Protected paths](../user/admin_area/settings/protected_paths.md).
- [Import/Export rate limits](../user/admin_area/settings/import_export_rate_limits.md).
## Rack Attack initializer

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,32 @@
---
type: reference
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
---
# Project/Group Import/Export rate limits
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2.
The following table includes configurable rate limits. The following table includes limits on a
per minute per user basis:
| Limit | Default (per minute per user) |
|--------------------------|-------------------------------|
| Project Import | 6 |
| Project Export | 6 |
| Project Export Download | 1 |
| Group Import | 6 |
| Group Export | 6 |
| Group Export Download | 1 |
All rate limits are:
- Configurable at **(admin)** **Admin Area > Settings > Network > Import/Export Rate Limits**
- Applied per minute per user
- Not applied per IP address
- Active by default. To disable, set the option to `0`
- Logged to `auth.log` file if exceed rate limit
![Import/Export rate limits](img/import_export_rate_limits_v13_2.png)

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This setting allows you to rate limit the requests to the issue creation endpoint.
It defaults to 300 requests per minute.
You can change it in **Admin Area > Settings > Network > Performance Optimization**.
You can change it in **Admin Area > Settings > Network > Issues Rate Limits**.
For example, requests using the
[Projects::IssuesController#create](https://gitlab.com/gitlab-org/gitlab/raw/master/app/controllers/projects/issues_controller.rb)

View File

@ -125,10 +125,10 @@ The repository will push soon. To force a push, click the appropriate button.
## Setting up a push mirror to another GitLab instance with 2FA activated
1. On the destination GitLab instance, create a [personal access token](../../profile/personal_access_tokens.md) with `API` scope.
1. On the destination GitLab instance, create a [personal access token](../../profile/personal_access_tokens.md) with `write_repository` scope.
1. On the source GitLab instance:
1. Fill in the **Git repository URL** field using this format: `https://oauth2@<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
1. Fill in **Password** field with the GitLab personal access token created on the destination GitLab instance.
1. Fill in the **Password** field with the GitLab personal access token created on the destination GitLab instance.
1. Click the **Mirror repository** button.
## Pulling from a remote repository **(STARTER)**

View File

@ -82,7 +82,9 @@ module.exports = path => {
'^.+\\.js$': 'babel-jest',
'^.+\\.vue$': 'vue-jest',
},
transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor)/)'],
transformIgnorePatterns: [
'node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor|monaco-yaml)/)',
],
timers: 'fake',
testEnvironment: '<rootDir>/spec/frontend/environment.js',
testEnvironmentOptions: {

View File

@ -19,17 +19,17 @@ module Gitlab
# and only do that when it's needed.
def rate_limits
{
issues_create: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.issues_create_limit }, interval: 1.minute },
project_export: { threshold: 30, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute },
project_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
project_download_export: { threshold: -> { application_settings.project_download_export_limit }, interval: 1.minute },
project_repositories_archive: { threshold: 5, interval: 1.minute },
project_generate_new_export: { threshold: 30, interval: 5.minutes },
project_import: { threshold: 30, interval: 5.minutes },
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.raw_blob_request_limit }, interval: 1.minute },
group_export: { threshold: 30, interval: 5.minutes },
group_download_export: { threshold: 10, interval: 10.minutes },
group_import: { threshold: 30, interval: 5.minutes }
project_generate_new_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute },
project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute },
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute }
}.freeze
end
@ -130,6 +130,10 @@ module Gitlab
"application_rate_limiter:#{serialized}"
end
def application_settings
Gitlab::CurrentSettings.current_application_settings
end
end
end
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class DigestColumn
class PersonalAccessToken < ActiveRecord::Base
self.table_name = 'personal_access_tokens'
end
def perform(model, attribute_from, attribute_to, start_id, stop_id)
model = model.constantize if model.is_a?(String)
model.transaction do
relation = model.where(id: start_id..stop_id).where.not(attribute_from => nil).lock
relation.each do |instance|
instance.update_columns(attribute_to => Gitlab::CryptoHelper.sha256(instance.read_attribute(attribute_from)),
attribute_from => nil)
end
end
end
end
end
end

View File

@ -1,104 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# EncryptColumn migrates data from an unencrypted column - `foo`, say - to
# an encrypted column - `encrypted_foo`, say.
#
# To avoid depending on a particular version of the model in app/, add a
# model to `lib/gitlab/background_migration/models/encrypt_columns` and use
# it in the migration that enqueues the jobs, so code can be shared.
#
# For this background migration to work, the table that is migrated _has_ to
# have an `id` column as the primary key. Additionally, the encrypted column
# should be managed by attr_encrypted, and map to an attribute with the same
# name as the unencrypted column (i.e., the unencrypted column should be
# shadowed), unless you want to define specific methods / accessors in the
# temporary model in `/models/encrypt_columns/your_model.rb`.
#
class EncryptColumns
def perform(model, attributes, from, to)
model = model.constantize if model.is_a?(String)
# If sidekiq hasn't undergone a restart, its idea of what columns are
# present may be inaccurate, so ensure this is as fresh as possible
model.reset_column_information
model.define_attribute_methods
attributes = expand_attributes(model, Array(attributes).map(&:to_sym))
model.transaction do
# Use SELECT ... FOR UPDATE to prevent the value being changed while
# we are encrypting it
relation = model.where(id: from..to).lock
relation.each do |instance|
encrypt!(instance, attributes)
end
end
end
def clear_migrated_values?
true
end
private
# Build a hash of { attribute => encrypted column name }
def expand_attributes(klass, attributes)
expanded = attributes.flat_map do |attribute|
attr_config = klass.encrypted_attributes[attribute]
crypt_column_name = attr_config&.fetch(:attribute)
raise "Couldn't determine encrypted column for #{klass}##{attribute}" if
crypt_column_name.nil?
raise "#{klass} source column: #{attribute} is missing" unless
klass.column_names.include?(attribute.to_s)
# Running the migration without the destination column being present
# leads to data loss
raise "#{klass} destination column: #{crypt_column_name} is missing" unless
klass.column_names.include?(crypt_column_name.to_s)
[attribute, crypt_column_name]
end
Hash[*expanded]
end
# Generate ciphertext for each column and update the database
def encrypt!(instance, attributes)
to_clear = attributes
.map { |plain, crypt| apply_attribute!(instance, plain, crypt) }
.compact
.flat_map { |plain| [plain, nil] }
to_clear = Hash[*to_clear]
if instance.changed?
instance.save!
if clear_migrated_values?
instance.update_columns(to_clear)
end
end
end
def apply_attribute!(instance, plain_column, crypt_column)
plaintext = instance[plain_column]
ciphertext = instance[crypt_column]
# No need to do anything if the plaintext is nil, or an encrypted
# value already exists
return unless plaintext.present?
return if ciphertext.present?
# attr_encrypted will calculate and set the expected value for us
instance.public_send("#{plain_column}=", plaintext) # rubocop:disable GitlabSecurity/PublicSend
plain_column
end
end
end
end

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# EncryptColumn migrates data from an unencrypted column - `foo`, say - to
# an encrypted column - `encrypted_foo`, say.
#
# We only create a subclass here because we want to isolate this migration
# (migrating unencrypted runner registration tokens to encrypted columns)
# from other `EncryptColumns` migration. This class name is going to be
# serialized and stored in Redis and later picked by Sidekiq, so we need to
# create a separate class name in order to isolate these migration tasks.
#
# We can solve this differently, see tech debt issue:
#
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54328
#
class EncryptRunnersTokens < EncryptColumns
def perform(model, from, to)
resource = "::Gitlab::BackgroundMigration::Models::EncryptColumns::#{model.to_s.capitalize}"
model = resource.constantize
attributes = model.encrypted_attributes.keys
super(model, attributes, from, to)
end
def clear_migrated_values?
false
end
end
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module Models
module EncryptColumns
# This model is shared between synchronous and background migrations to
# encrypt the `runners_token` column in `namespaces` table.
#
class Namespace < ActiveRecord::Base
include ::EachBatch
self.table_name = 'namespaces'
self.inheritance_column = :_type_disabled
def runners_token=(value)
self.runners_token_encrypted =
::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
end
def self.encrypted_attributes
{ runners_token: { attribute: :runners_token_encrypted } }
end
end
end
end
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module Models
module EncryptColumns
# This model is shared between synchronous and background migrations to
# encrypt the `runners_token` column in `projects` table.
#
class Project < ActiveRecord::Base
include ::EachBatch
self.table_name = 'projects'
self.inheritance_column = :_type_disabled
def runners_token=(value)
self.runners_token_encrypted =
::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
end
def self.encrypted_attributes
{ runners_token: { attribute: :runners_token_encrypted } }
end
end
end
end
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module Models
module EncryptColumns
# This model is shared between synchronous and background migrations to
# encrypt the `token` column in `ci_runners` table.
#
class Runner < ActiveRecord::Base
include ::EachBatch
self.table_name = 'ci_runners'
self.inheritance_column = :_type_disabled
def token=(value)
self.token_encrypted =
::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
end
def self.encrypted_attributes
{ token: { attribute: :token_encrypted } }
end
end
end
end
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module Models
module EncryptColumns
# This model is shared between synchronous and background migrations to
# encrypt the `runners_token` column in `application_settings` table.
#
class Settings < ActiveRecord::Base
include ::EachBatch
include ::CacheableAttributes
self.table_name = 'application_settings'
self.inheritance_column = :_type_disabled
after_commit do
::ApplicationSetting.expire
end
def runners_registration_token=(value)
self.runners_registration_token_encrypted =
::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
end
def self.encrypted_attributes
{
runners_registration_token: {
attribute: :runners_registration_token_encrypted
}
}
end
end
end
end
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module Models
module EncryptColumns
# This model is shared between synchronous and background migrations to
# encrypt the `token` and `url` columns
class WebHook < ActiveRecord::Base
include ::EachBatch
self.table_name = 'web_hooks'
self.inheritance_column = :_type_disabled
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: ::Settings.attr_encrypted_db_key_base_32
attr_encrypted :url,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: ::Settings.attr_encrypted_db_key_base_32
end
end
end
end
end

View File

@ -5,7 +5,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
include Gitlab::Routing
LATEST_COMMITS_LIMIT = 10
LATEST_COMMITS_LIMIT = 2
def initialize(project, commit_sha)
@project = project

View File

@ -8,9 +8,11 @@ module Gitlab
MAX_MSG_SIZE = 128.kilobytes.freeze
def self.exists?(remote_url)
request = Gitaly::FindRemoteRepositoryRequest.new(remote: remote_url)
storage = GitalyClient.random_storage
response = GitalyClient.call(GitalyClient.random_storage,
request = Gitaly::FindRemoteRepositoryRequest.new(remote: remote_url, storage_name: storage)
response = GitalyClient.call(storage,
:remote_service,
:find_remote_repository, request,
timeout: GitalyClient.medium_timeout)

View File

@ -82,7 +82,7 @@ module Gitlab
end
def count_request
@request_counter ||= Gitlab::Metrics.counter(:redis_client_requests_total, 'Client side Redis request count, per Redis server')
@request_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_requests_total, 'Client side Redis request count, per Redis server')
@request_counter.increment({ storage: storage_key })
end
@ -90,7 +90,7 @@ module Gitlab
# This metric is meant to give a client side view of how the Redis
# server is doing. Redis itself does not expose error counts. This
# metric can be used for Redis alerting and service health monitoring.
@exception_counter ||= Gitlab::Metrics.counter(:redis_client_exceptions_total, 'Client side Redis exception count, per Redis server, per exception class')
@exception_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_exceptions_total, 'Client side Redis exception count, per Redis server, per exception class')
@exception_counter.increment({ storage: storage_key, exception: ex.class.to_s })
end

View File

@ -93,8 +93,14 @@ module Quality
private
def migration_and_background_migration_folders
TEST_LEVEL_FOLDERS.fetch(:migration) + TEST_LEVEL_FOLDERS.fetch(:background_migration)
end
def folders_pattern(level)
case level
when :migration
"{#{migration_and_background_migration_folders.join(',')}}"
# Geo specs aren't in a specific folder, but they all have the :geo tag, so we must search for them globally
when :all, :geo
'**'
@ -105,6 +111,8 @@ module Quality
def folders_regex(level)
case level
when :migration
"(#{migration_and_background_migration_folders.join('|')})"
# Geo specs aren't in a specific folder, but they all have the :geo tag, so we must search for them globally
when :all, :geo
''

View File

@ -192,6 +192,11 @@ msgid_plural "%d issues selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue successfully imported with the label"
msgid_plural "%d issues successfully imported with the label"
msgstr[0] ""
msgstr[1] ""
msgid "%d layer"
msgid_plural "%d layers"
msgstr[0] ""
@ -6082,6 +6087,9 @@ msgstr ""
msgid "Configure limit for issues created per minute by web and API requests."
msgstr ""
msgid "Configure limits for Project/Group Import/Export."
msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
@ -12226,6 +12234,9 @@ msgstr ""
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgstr ""
msgid "Import/Export Rate Limits"
msgstr ""
msgid "Import/Export illustration"
msgstr ""
@ -12756,9 +12767,6 @@ msgstr ""
msgid "Issues referenced by merge requests and commits within the default branch will be closed automatically"
msgstr ""
msgid "Issues successfully imported with the label"
msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr ""
@ -13989,6 +13997,24 @@ msgstr ""
msgid "Maven Metadata"
msgstr ""
msgid "Max Group Export Download requests per minute per user"
msgstr ""
msgid "Max Group Export requests per minute per user"
msgstr ""
msgid "Max Group Import requests per minute per user"
msgstr ""
msgid "Max Project Export Download requests per minute per user"
msgstr ""
msgid "Max Project Export requests per minute per user"
msgstr ""
msgid "Max Project Import requests per minute per user"
msgstr ""
msgid "Max access level"
msgstr ""
@ -19505,6 +19531,9 @@ msgstr ""
msgid "Required approvals (%{approvals_given} given, you've approved)"
msgstr ""
msgid "Required in this project."
msgstr ""
msgid "Requirement"
msgstr ""
@ -22379,9 +22408,6 @@ msgstr ""
msgid "Suggestions must all be on the same branch."
msgstr ""
msgid "Suggestions that change line count can't be added to batches, yet."
msgstr ""
msgid "Suggestions:"
msgstr ""
@ -23397,7 +23423,7 @@ msgstr ""
msgid "This also resolves all related threads"
msgstr ""
msgid "This also resolves the discussion"
msgid "This also resolves this thread"
msgstr ""
msgid "This application was created by %{link_to_owner}."

View File

@ -97,6 +97,7 @@
"jquery.caret": "^0.3.1",
"jquery.waitforimages": "^2.2.0",
"js-cookie": "^2.2.1",
"js-yaml": "^3.13.1",
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
@ -105,8 +106,9 @@
"mermaid": "^8.5.2",
"mersenne-twister": "1.1.0",
"minimatch": "^3.0.4",
"monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.0",
"monaco-yaml": "^2.4.0",
"mousetrap": "^1.4.6",
"pdfjs-dist": "^2.0.943",
"pikaday": "^1.8.0",
@ -220,7 +222,7 @@
},
"resolutions": {
"chokidar": "^3.4.0",
"monaco-editor": "0.18.1",
"monaco-editor": "0.20.0",
"vue-jest/ts-jest": "24.0.0"
},
"engines": {

View File

@ -38,7 +38,7 @@ module QA
# Note: This test doesn't have the :orchestrated tag because it runs in the Test::Integration::Praefect
# scenario with other tests that aren't considered orchestrated.
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect do
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/227127', type: :investigating } do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'repo-storage-move'

View File

@ -941,7 +941,7 @@ RSpec.describe GroupsController do
allow(Gitlab::ApplicationRateLimiter)
.to receive(:increment)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold] + 1)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1)
end
it 'throttles the endpoint' do
@ -1015,7 +1015,7 @@ RSpec.describe GroupsController do
allow(Gitlab::ApplicationRateLimiter)
.to receive(:increment)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_download_export][:threshold] + 1)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_download_export][:threshold].call + 1)
end
it 'throttles the endpoint' do

View File

@ -1197,7 +1197,7 @@ RSpec.describe ProjectsController do
before do
allow(Gitlab::ApplicationRateLimiter)
.to receive(:increment)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits["project_#{action}".to_sym][:threshold] + 1)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits["project_#{action}".to_sym][:threshold].call + 1)
end
it 'prevents requesting project export' do
@ -1264,7 +1264,7 @@ RSpec.describe ProjectsController do
before do
allow(Gitlab::ApplicationRateLimiter)
.to receive(:increment)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold] + 1)
.and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call + 1)
end
it 'prevents requesting project export' do

View File

@ -8,9 +8,11 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import 'monaco-editor/esm/vs/language/html/monaco.contribution';
import 'monaco-editor/esm/vs/basic-languages/monaco.contribution';
import 'monaco-yaml/esm/monaco.contribution';
// This language starts trying to spin up web workers which obviously breaks in Jest environment
jest.mock('monaco-editor/esm/vs/language/typescript/tsMode');
jest.mock('monaco-yaml/esm/yamlMode');
export * from 'monaco-editor/esm/vs/editor/editor.api';
export default global.monaco;

View File

@ -637,6 +637,8 @@ describe('RepoEditor', () => {
// set cursor to line 2, column 1
vm.editor.instance.setSelection(new Range(2, 1, 2, 1));
vm.editor.instance.focus();
jest.spyOn(vm.editor.instance, 'hasTextFocus').mockReturnValue(true);
});
});

View File

@ -199,6 +199,14 @@ describe('Multi-file editor library', () => {
});
});
describe('schemas', () => {
it('registers custom schemas defined with Monaco', () => {
expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
});
});
});
describe('replaceSelectedText', () => {
let model;
let editor;

View File

@ -1,6 +1,7 @@
import {
isTextFile,
registerLanguages,
registerSchemas,
trimPathComponents,
insertFinalNewline,
trimTrailingWhitespace,
@ -158,6 +159,57 @@ describe('WebIDE utils', () => {
});
});
describe('registerSchemas', () => {
let options;
beforeEach(() => {
options = {
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [
{
uri: 'http://myserver/foo-schema.json',
fileMatch: ['*'],
schema: {
id: 'http://myserver/foo-schema.json',
type: 'object',
properties: {
p1: { enum: ['v1', 'v2'] },
p2: { $ref: 'http://myserver/bar-schema.json' },
},
},
},
{
uri: 'http://myserver/bar-schema.json',
schema: {
id: 'http://myserver/bar-schema.json',
type: 'object',
properties: { q1: { enum: ['x1', 'x2'] } },
},
},
],
};
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
});
it.each`
language | defaultsObj
${'json'} | ${languages.json.jsonDefaults}
${'yaml'} | ${languages.yaml.yamlDefaults}
`(
'registers the given schemas with monaco for lang: $language',
({ language, defaultsObj }) => {
registerSchemas({ language, options });
expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options);
},
);
});
describe('trimTrailingWhitespace', () => {
it.each`
input | output

View File

@ -33,6 +33,7 @@ describe('IssuableListRootApp', () => {
isFinishedAlertShowing,
isInProgressAlertShowing,
jiraImport: {
importedIssuesCount: 1,
isInProgress,
isFinished,
label,
@ -77,7 +78,7 @@ describe('IssuableListRootApp', () => {
describe('shows an alert', () => {
it('tells the user the Jira import has finished', () => {
expect(findAlert().text()).toBe('Issues successfully imported with the label');
expect(findAlert().text()).toBe('1 issue successfully imported with the label');
});
it('contains the label title associated with the Jira import', () => {

View File

@ -19,6 +19,8 @@ describe('Squash before merge component', () => {
wrapper.destroy();
});
const findLabel = () => wrapper.find('[data-testid="squashLabel"]');
describe('checkbox', () => {
const findCheckbox = () => wrapper.find('.js-squash-checkbox');
@ -64,8 +66,6 @@ describe('Squash before merge component', () => {
});
describe('label', () => {
const findLabel = () => wrapper.find('[data-testid="squashLabel"]');
describe.each`
isDisabled | expectation
${true} | ${'grays out text if it is true'}
@ -84,6 +84,27 @@ describe('Squash before merge component', () => {
});
});
describe('tooltip', () => {
const tooltipTitle = () => findLabel().element.dataset.title;
it('does not render when isDisabled is false', () => {
createComponent({
value: true,
isDisabled: false,
});
expect(tooltipTitle()).toBeUndefined();
});
it('display message when when isDisabled is true', () => {
createComponent({
value: true,
isDisabled: true,
});
expect(tooltipTitle()).toBe('Required in this project.');
});
});
describe('about link', () => {
it('is not rendered if no help path is passed', () => {
createComponent({

View File

@ -71,7 +71,7 @@ describe('Suggestion Diff component', () => {
it('renders correct tooltip message for apply button', () => {
createComponent();
expect(wrapper.vm.tooltipMessage).toBe('This also resolves the discussion');
expect(wrapper.vm.tooltipMessage).toBe('This also resolves this thread');
});
describe('when apply suggestion is clicked', () => {
@ -227,11 +227,10 @@ describe('Suggestion Diff component', () => {
createComponent({ canApply: false });
});
it('disables apply suggestion and add to batch buttons', () => {
it('disables apply suggestion and hides add to batch button', () => {
expect(findApplyButton().exists()).toBe(true);
expect(findAddToBatchButton().exists()).toBe(true);
expect(findAddToBatchButton().exists()).toBe(false);
expect(findApplyButton().attributes('disabled')).toBe('true');
expect(findAddToBatchButton().attributes('disabled')).toBe('true');
});
it('renders correct tooltip message for apply button', () => {

View File

@ -6,25 +6,6 @@ const buildMockTextNode = literal => {
};
};
const buildMockTextNodeWithAdjacentInlineCode = isForward => {
const direction = isForward ? 'next' : 'prev';
const literalOpen = '[';
const literalClose = ' file]: https://file.com/file.md';
return {
literal: isForward ? literalOpen : literalClose,
type: 'text',
[direction]: {
literal: 'raw',
tickCount: 1,
type: 'code',
[direction]: {
literal: isForward ? literalClose : literalOpen,
[direction]: null,
},
},
};
};
const buildMockListNode = literal => {
return {
firstChild: {
@ -38,15 +19,23 @@ const buildMockListNode = literal => {
};
};
export const buildMockParagraphNode = literal => {
return {
firstChild: buildMockTextNode(literal),
type: 'paragraph',
};
};
export const kramdownListNode = buildMockListNode('TOC');
export const normalListNode = buildMockListNode('Just another bullet point');
export const kramdownTextNode = buildMockTextNode('{:toc}');
export const identifierTextNode = buildMockTextNode('[Some text]: https://link.com');
export const identifierInlineCodeTextEnteringNode = buildMockTextNodeWithAdjacentInlineCode(true);
export const identifierInlineCodeTextExitingNode = buildMockTextNodeWithAdjacentInlineCode(false);
export const embeddedRubyTextNode = buildMockTextNode('<%= partial("some/path") %>');
export const normalTextNode = buildMockTextNode('This is just normal text.');
export const normalParagraphNode = buildMockParagraphNode(
'This is just normal paragraph. It has multiple sentences.',
);
const uneditableOpenToken = {
type: 'openTag',

View File

@ -0,0 +1,55 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph';
import {
buildUneditableOpenTokens,
buildUneditableCloseToken,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { buildMockParagraphNode, normalParagraphNode } from '../../mock_data';
const identifierParagraphNode = buildMockParagraphNode(
`[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
);
describe('Render Identifier Paragraph renderer', () => {
describe('canRender', () => {
it.each`
node | paragraph | target
${identifierParagraphNode} | ${'[Some text]: https://link.com'} | ${true}
${normalParagraphNode} | ${'Normal non-identifier text. Another sentence.'} | ${false}
`(
'should return $target when the $node matches $paragraph syntax',
({ node, paragraph, target }) => {
const context = {
entering: true,
getChildrenText: jest.fn().mockReturnValueOnce(paragraph),
};
expect(renderer.canRender(node, context)).toBe(target);
},
);
});
describe('render', () => {
let origin;
beforeEach(() => {
origin = jest.fn();
});
it('should return uneditable open tokens when entering', () => {
const context = { entering: true, origin };
expect(renderer.render(identifierParagraphNode, context)).toStrictEqual(
buildUneditableOpenTokens(origin()),
);
});
it('should return an uneditable close tokens when exiting', () => {
const context = { entering: false, origin };
expect(renderer.render(identifierParagraphNode, context)).toStrictEqual(
buildUneditableCloseToken(origin()),
);
});
});
});

View File

@ -1,61 +0,0 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_text';
import {
buildUneditableOpenTokens,
buildUneditableCloseTokens,
buildUneditableTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import {
identifierTextNode,
identifierInlineCodeTextEnteringNode,
identifierInlineCodeTextExitingNode,
normalTextNode,
} from '../../mock_data';
describe('Render Identifier Text renderer', () => {
describe('canRender', () => {
it('should return true when the argument `literal` has identifier syntax', () => {
expect(renderer.canRender(identifierTextNode)).toBe(true);
});
it('should return true when the argument `literal` has identifier syntax and forward adjacent inline code', () => {
expect(renderer.canRender(identifierInlineCodeTextEnteringNode)).toBe(true);
});
it('should return true when the argument `literal` has identifier syntax and backward adjacent inline code', () => {
expect(renderer.canRender(identifierInlineCodeTextExitingNode)).toBe(true);
});
it('should return false when the argument `literal` lacks identifier syntax', () => {
expect(renderer.canRender(normalTextNode)).toBe(false);
});
});
describe('render', () => {
const origin = jest.fn();
it('should return uneditable tokens for basic identifier syntax', () => {
const context = { origin };
expect(renderer.render(identifierTextNode, context)).toStrictEqual(
buildUneditableTokens(origin()),
);
});
it('should return uneditable open tokens for non-basic inline code identifier syntax when entering', () => {
const context = { origin };
expect(renderer.render(identifierInlineCodeTextEnteringNode, context)).toStrictEqual(
buildUneditableOpenTokens(origin()),
);
});
it('should return uneditable close tokens for non-basic inline code identifier syntax when exiting', () => {
const context = { origin };
expect(renderer.render(identifierInlineCodeTextExitingNode, context)).toStrictEqual(
buildUneditableCloseTokens(origin()),
);
});
});
});

View File

@ -2,13 +2,15 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount, schema: 20180105212544 do
RSpec.describe Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:merge_requests_table) { table(:merge_requests) }
let(:merge_request_diffs_table) { table(:merge_request_diffs) }
let(:merge_request_diff_commits_table) { table(:merge_request_diff_commits) }
let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
let(:namespace) { namespaces_table.create!(name: 'gitlab-org', path: 'gitlab-org') }
let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: namespace.id) }
let(:merge_request) do
merge_requests_table.create!(target_project_id: project.id,
target_branch: 'master',

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