Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dfb41a436c
commit
5ff17740d4
|
|
@ -16,4 +16,4 @@ variables:
|
|||
QA_OMNIBUS_MR_TESTS: "only-smoke"
|
||||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
GITLAB_HELM_CHART_REF: "ec4eb83b98572fd2721516df00799858512b0538" # helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "8590da829037a326808e425488919d374ed36cd2" # helm chart ref used by test-on-cng pipeline
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default {
|
|||
<template #left-primary>
|
||||
<gl-link
|
||||
:href="report.reportPath"
|
||||
class="gl-font-normal gl-pt-4 gl-text-gray-900"
|
||||
class="gl-font-normal gl-text-gray-900"
|
||||
data-testid="abuse-report-title"
|
||||
>
|
||||
{{ title }}
|
||||
|
|
@ -86,7 +86,7 @@ export default {
|
|||
</template>
|
||||
|
||||
<template #right-secondary>
|
||||
<div class="gl-mt-7" data-testid="abuse-report-date">{{ displayDate }}</div>
|
||||
<div data-testid="abuse-report-date">{{ displayDate }}</div>
|
||||
</template>
|
||||
</list-item>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ class TOCHeading {
|
|||
parent = null;
|
||||
subHeadings = [];
|
||||
|
||||
constructor(text) {
|
||||
constructor(text, href) {
|
||||
this.text = text;
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
get level() {
|
||||
return this.parent ? this.parent.level + 1 : 0;
|
||||
}
|
||||
|
||||
addSubHeading(text) {
|
||||
const heading = new TOCHeading(text);
|
||||
addSubHeading(text, href) {
|
||||
const heading = new TOCHeading(text, href);
|
||||
heading.parent = this;
|
||||
this.subHeadings.push(heading);
|
||||
return heading;
|
||||
|
|
@ -46,6 +47,7 @@ class TOCHeading {
|
|||
toJSON() {
|
||||
return {
|
||||
text: this.text,
|
||||
href: this.href,
|
||||
level: this.level,
|
||||
subHeadings: this.subHeadings.map((subHeading) => subHeading.toJSON()),
|
||||
};
|
||||
|
|
@ -76,7 +78,7 @@ export function toTree(headings) {
|
|||
if (heading.level <= currentHeading.level) {
|
||||
currentHeading = currentHeading.parentAt(heading.level - 1);
|
||||
}
|
||||
currentHeading = (currentHeading || tree).addSubHeading(heading.text);
|
||||
currentHeading = (currentHeading || tree).addSubHeading(heading.text, heading.href);
|
||||
}
|
||||
|
||||
return tree.flattenIfEmpty().toJSON();
|
||||
|
|
@ -98,3 +100,15 @@ export function getHeadings(editor) {
|
|||
|
||||
return toTree(headings).subHeadings;
|
||||
}
|
||||
|
||||
export function getHeadingsFromDOM(containerElement) {
|
||||
const headingSelectors = 'h1, h2, h3, h4, h5, h6';
|
||||
|
||||
return toTree(
|
||||
[...containerElement.querySelectorAll(headingSelectors)].map((heading) => ({
|
||||
level: parseInt(heading.tagName[1], 10),
|
||||
text: heading.textContent.trim(),
|
||||
href: heading.querySelector('a').getAttribute('href'),
|
||||
})),
|
||||
).subHeadings;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import TableOfContentsHeading from './table_of_contents_heading.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TableOfContentsHeading,
|
||||
},
|
||||
props: {
|
||||
headings: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="toc"
|
||||
class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-p-3 gl-m-3 gl-bg-gray-10"
|
||||
>
|
||||
<strong class="gl-text-sm gl-py-3">{{ __('On this page') }}</strong>
|
||||
<ul class="wiki-pages gl-text-sm">
|
||||
<table-of-contents-heading
|
||||
v-for="(heading, index) in headings"
|
||||
:key="index"
|
||||
:heading="heading"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'TableOfContentsHeading',
|
||||
props: {
|
||||
heading: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li dir="auto">
|
||||
<a :href="heading.href" class="gl-str-truncated">
|
||||
{{ heading.text }}
|
||||
</a>
|
||||
|
||||
<ul v-if="heading.subHeadings.length" dir="auto" class="!gl-pl-3">
|
||||
<table-of-contents-heading
|
||||
v-for="(child, index) in heading.subHeadings"
|
||||
:key="index"
|
||||
:heading="child"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
|
@ -6,6 +7,10 @@ import { handleLocationHash } from '~/lib/utils/common_utils';
|
|||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { __ } from '~/locale';
|
||||
import { getHeadingsFromDOM } from '~/content_editor/services/table_of_contents_utils';
|
||||
import TableOfContents from './table_of_contents.vue';
|
||||
|
||||
const TableOfContentsComponent = Vue.extend(TableOfContents);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -22,12 +27,24 @@ export default {
|
|||
content: '',
|
||||
isLoadingContent: false,
|
||||
loadingContentFailed: false,
|
||||
headings: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadWikiContent();
|
||||
},
|
||||
methods: {
|
||||
async renderHeadingsInSidebar() {
|
||||
const headings = getHeadingsFromDOM(this.$refs.content);
|
||||
if (!headings.length) return;
|
||||
|
||||
const tocComponent = new TableOfContentsComponent({ propsData: { headings } }).$mount();
|
||||
const tocContainer = document.querySelector('.js-wiki-toc');
|
||||
|
||||
tocContainer.innerHTML = '';
|
||||
tocContainer.appendChild(tocComponent.$el);
|
||||
},
|
||||
|
||||
async loadWikiContent() {
|
||||
this.loadingContentFailed = false;
|
||||
this.isLoadingContent = true;
|
||||
|
|
@ -42,12 +59,14 @@ export default {
|
|||
.then(() => {
|
||||
renderGFM(this.$refs.content);
|
||||
handleLocationHash();
|
||||
|
||||
this.renderHeadingsInSidebar();
|
||||
})
|
||||
.catch(() =>
|
||||
.catch(() => {
|
||||
createAlert({
|
||||
message: this.$options.i18n.renderingContentFailed,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
this.loadingContentFailed = true;
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export default {
|
|||
<gl-collapse
|
||||
:id="itemId"
|
||||
v-model="isExpanded"
|
||||
class="gl-list-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease"
|
||||
class="gl-list-none gl-p-0 gl-m-0 gl-duration-medium gl-ease-ease"
|
||||
data-testid="menu-section"
|
||||
:data-qa-section-name="item.title"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export default {
|
|||
v-if="hoverMap[`code-${index}`]"
|
||||
:title="$options.i18n.copyCodeTitle"
|
||||
:text="block.text"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-z-1 gl-transition-duration-medium"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-z-1 gl-duration-medium"
|
||||
/>
|
||||
<code-block-highlighted
|
||||
class="gl-border gl-rounded-0! gl-p-4 gl-mb-0 gl-overflow-y-auto"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.v
|
|||
import WorkItemTreeChildren from './work_item_tree_children.vue';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemLinkChild',
|
||||
components: {
|
||||
GlButton,
|
||||
WorkItemTreeChildren,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(event, child) {
|
||||
// To avoid incorrect work item to be bubbled up
|
||||
// Assign the correct child item
|
||||
if (!event.childItem) {
|
||||
Object.assign(event, { childItem: child });
|
||||
}
|
||||
this.$emit('click', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -42,7 +52,7 @@ export default {
|
|||
:work-item-type="workItemType"
|
||||
:show-labels="showLabels"
|
||||
@removeChild="$emit('removeChild', $event)"
|
||||
@click="$emit('click', Object.assign($event, { childItem: child }))"
|
||||
@click="onClick($event, child)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
}
|
||||
|
||||
.upload-dropzone-card {
|
||||
transition: background $gl-transition-duration-medium $general-hover-transition-curve,
|
||||
border $gl-transition-duration-medium $general-hover-transition-curve;
|
||||
@apply gl-transition-[background,border];
|
||||
|
||||
color: $gl-text-color;
|
||||
|
||||
&:hover,
|
||||
|
|
@ -53,7 +53,9 @@
|
|||
|
||||
.upload-dropzone-fade-enter-active,
|
||||
.upload-dropzone-fade-leave-active {
|
||||
transition: opacity $general-hover-transition-duration $general-hover-transition-curve;
|
||||
@apply gl-transition-opacity;
|
||||
@apply gl-duration-fast;
|
||||
@apply gl-ease-linear;
|
||||
}
|
||||
|
||||
.upload-dropzone-fade-enter,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
box-shadow: none;
|
||||
width: 100%;
|
||||
resize: none !important;
|
||||
transition: box-shadow $gl-transition-duration-medium ease;
|
||||
@apply gl-transition-box-shadow;
|
||||
}
|
||||
|
||||
.md-suggestion-diff {
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@
|
|||
}
|
||||
|
||||
@mixin side-panel-toggle {
|
||||
transition: width $gl-transition-duration-medium;
|
||||
@apply gl-transition-width;
|
||||
height: $toggle-sidebar-height;
|
||||
padding: 0 $gl-padding;
|
||||
background-color: $gray-10;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
.page-initialised .content-wrapper {
|
||||
transition: padding $gl-transition-duration-medium;
|
||||
@apply gl-transition-padding;
|
||||
}
|
||||
|
||||
.right-sidebar-collapsed {
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
@include maintain-sidebar-dimensions;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
transition: width $gl-transition-duration-medium;
|
||||
@apply gl-transition-width;
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
@include maintain-sidebar-dimensions;
|
||||
|
|
@ -321,10 +321,10 @@
|
|||
position: fixed;
|
||||
bottom: calc(#{$calc-application-footer-height} + var(--mr-review-bar-height));
|
||||
right: 0;
|
||||
transition: width $gl-transition-duration-medium;
|
||||
background-color: $white;
|
||||
z-index: 200;
|
||||
overflow: hidden;
|
||||
@apply gl-transition-width;
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
$super-sidebar-transition-delay: 0.4s;
|
||||
$super-sidebar-transition-duration: $gl-transition-duration-medium;
|
||||
$super-sidebar-transition-duration: 0.2s;
|
||||
$super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
|
||||
$command-palette-spacing: px-to-rem(14px);
|
||||
|
||||
|
|
@ -335,7 +335,7 @@ $command-palette-spacing: px-to-rem(14px);
|
|||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform $super-sidebar-transition-duration;
|
||||
@apply gl-transition-transform;
|
||||
}
|
||||
|
||||
.user-bar {
|
||||
|
|
@ -523,7 +523,7 @@ $command-palette-spacing: px-to-rem(14px);
|
|||
padding-left: 0;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: padding-left $super-sidebar-transition-duration;
|
||||
@apply gl-transition-padding;
|
||||
}
|
||||
|
||||
&:not(.page-with-super-sidebar-collapsed) {
|
||||
|
|
@ -661,7 +661,9 @@ $command-palette-spacing: px-to-rem(14px);
|
|||
|
||||
.transition-opacity-on-hover--context {
|
||||
.transition-opacity-on-hover--target {
|
||||
transition: opacity $gl-transition-duration-fast linear;
|
||||
@apply gl-transition-opacity;
|
||||
@apply gl-duration-fast;
|
||||
@apply gl-ease-linear;
|
||||
|
||||
&:hover {
|
||||
transition-delay: $super-sidebar-transition-delay;
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: left $gl-transition-duration-medium,
|
||||
right $gl-transition-duration-medium,
|
||||
width $gl-transition-duration-medium;
|
||||
@apply gl-transition-[width,left,right];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
.fade-leave-active,
|
||||
.fade-in-enter-active,
|
||||
.fade-out-leave-active {
|
||||
transition: opacity $gl-transition-duration-medium $general-hover-transition-curve;
|
||||
@apply gl-transition-opacity;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
flex-direction: column;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
transition: width $gl-transition-duration-medium;
|
||||
@apply gl-transition-width;
|
||||
width: 100%;
|
||||
|
||||
&.is-compact {
|
||||
|
|
|
|||
|
|
@ -182,9 +182,9 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
|||
.design-note {
|
||||
padding: $gl-padding-8;
|
||||
list-style: none;
|
||||
transition: background $gl-transition-duration-medium $general-hover-transition-curve;
|
||||
border-top-left-radius: $gl-border-radius-base; // same border radius used by .bordered-box
|
||||
border-top-right-radius: $gl-border-radius-base;
|
||||
@apply gl-transition-background;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -999,7 +999,7 @@
|
|||
padding-right: $right-sidebar-collapsed-width;
|
||||
background: var(--white, $white);
|
||||
border-top: 1px solid var(--border-color, $border-color);
|
||||
transition: padding $gl-transition-duration-medium;
|
||||
@apply gl-transition-padding;
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, sm)-1) {
|
||||
padding-left: 0;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
}
|
||||
|
||||
.sidebar-container {
|
||||
padding: 20px 0;
|
||||
padding-right: 100px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
|
|
|
|||
|
|
@ -14,17 +14,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
|
|||
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
|
||||
ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
|
||||
ignore_column :required_instance_ci_template, remove_with: '17.1', remove_after: '2024-05-10'
|
||||
ignore_columns %i[
|
||||
container_registry_import_max_tags_count
|
||||
container_registry_import_max_retries
|
||||
container_registry_import_start_max_retries
|
||||
container_registry_import_max_step_duration
|
||||
container_registry_pre_import_tags_rate
|
||||
container_registry_pre_import_timeout
|
||||
container_registry_import_timeout
|
||||
container_registry_import_target_plan
|
||||
container_registry_import_created_before
|
||||
], remove_with: '17.2', remove_after: '2024-06-24'
|
||||
ignore_column %i[sign_in_text help_text], remove_with: '17.3', remove_after: '2024-08-15'
|
||||
ignore_columns %i[toggle_security_policies_policy_scope lock_toggle_security_policies_policy_scope], remove_with: '17.2', remove_after: '2024-07-12'
|
||||
ignore_columns %i[arkose_labs_verify_api_url], remove_with: '17.4', remove_after: '2024-08-09'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Import
|
||||
class SourceUserPlaceholderReference < ApplicationRecord
|
||||
self.table_name = 'import_source_user_placeholder_references'
|
||||
|
||||
belongs_to :source_user, class_name: 'Import::SourceUser'
|
||||
belongs_to :namespace
|
||||
|
||||
validates :model, :namespace_id, :source_user_id, :user_reference_column, presence: true
|
||||
validates :numeric_key, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
||||
validates :composite_key,
|
||||
json_schema: { filename: 'import_source_user_placeholder_reference_composite_key' },
|
||||
allow_nil: true
|
||||
validate :validate_numeric_or_composite_key_present
|
||||
|
||||
attribute :composite_key, :ind_jsonb
|
||||
|
||||
private
|
||||
|
||||
def validate_numeric_or_composite_key_present
|
||||
return if numeric_key.present? ^ composite_key.present?
|
||||
|
||||
errors.add(:base, :blank, message: 'numeric_key or composite_key must be present')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -342,6 +342,8 @@ class Project < ApplicationRecord
|
|||
has_many :hooks, class_name: 'ProjectHook'
|
||||
has_many :protected_branches
|
||||
has_many :exported_protected_branches
|
||||
has_many :all_protected_branches, ->(project) { ProtectedBranch.unscope(:where).from_union(project.protected_branches, project.group_protected_branches) }, class_name: 'ProtectedBranch'
|
||||
|
||||
has_many :protected_tags
|
||||
has_many :repository_languages, -> { order "share DESC" }
|
||||
has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
|
||||
|
|
@ -1278,8 +1280,8 @@ class Project < ApplicationRecord
|
|||
|
||||
def preload_protected_branches
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [self],
|
||||
associations: { protected_branches: [:push_access_levels, :merge_access_levels] }
|
||||
records: [all_protected_branches, protected_branches].flatten,
|
||||
associations: [:push_access_levels, :merge_access_levels]
|
||||
).call
|
||||
end
|
||||
|
||||
|
|
@ -2948,15 +2950,9 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def group_protected_branches
|
||||
root_namespace.is_a?(Group) ? root_namespace.protected_branches : ProtectedBranch.none
|
||||
end
|
||||
return root_namespace.protected_branches if allow_protected_branches_for_group? && root_namespace.is_a?(Group)
|
||||
|
||||
def all_protected_branches
|
||||
if allow_protected_branches_for_group?
|
||||
@all_protected_branches ||= ProtectedBranch.from_union([protected_branches, group_protected_branches])
|
||||
else
|
||||
protected_branches
|
||||
end
|
||||
ProtectedBranch.none
|
||||
end
|
||||
|
||||
def allow_protected_branches_for_group?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Stores composite_key data for imported records that are mapped to placeholder users",
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
- page_title _('Abuse Reports')
|
||||
- page_title _('Abuse reports')
|
||||
|
||||
%h1.page-title.gl-font-size-h-display{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_abuse_reports_pageload' } }
|
||||
= _('Abuse Reports')
|
||||
= render ::Layouts::PageHeadingComponent.new(_('Abuse reports'), options: { data: { event_tracking_load: 'true', event_tracking: 'view_admin_abuse_reports_pageload' } })
|
||||
|
||||
#js-abuse-reports-list-app{ data: abuse_reports_list_data(@abuse_reports) }
|
||||
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
- add_to_breadcrumbs _('Abuse Reports'), admin_abuse_reports_path
|
||||
- add_to_breadcrumbs _('Abuse reports'), admin_abuse_reports_path
|
||||
- breadcrumb_title @abuse_report.user&.name
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- page_title @abuse_report.user&.name, _('Abuse Reports')
|
||||
- page_title @abuse_report.user&.name, _('Abuse reports')
|
||||
|
||||
#js-abuse-reports-detail-view{ data: abuse_report_data(@abuse_report) }
|
||||
= gl_loading_icon(css_class: 'gl-my-5', size: 'md')
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@
|
|||
|
||||
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" }, 'aria-label': _('Wiki') }
|
||||
.sidebar-container
|
||||
.block.gl-mb-3.gl-mx-5.gl-block.sm:gl-hidden{ class: '!gl-pt-0' }
|
||||
.block.gl-mb-3.gl-mx-5.gl-mt-5.gl-block.sm:gl-hidden{ class: '!gl-pt-0' }
|
||||
%a.gutter-toggle.gl-float-right.gl-block.md:gl-hidden.js-sidebar-wiki-toggle{ href: "#" }
|
||||
= sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
|
||||
|
||||
- if @sidebar_error.present?
|
||||
= render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')
|
||||
|
||||
.blocks-container{ class: '!gl-px-3' }
|
||||
- if !editing
|
||||
.js-wiki-toc
|
||||
|
||||
.blocks-container{ class: '!gl-px-3 !gl-my-5' }
|
||||
.gl-flex.gl-place-content-between.gl-items-center.gl-pb-3.gl-pr-1{ class: (@sidebar_page ? 'js-wiki-expand-pages-list wiki-list collapsed gl-pl-0' : 'gl-pl-3') }
|
||||
.gl-flex.gl-items-center
|
||||
- if @sidebar_page
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ module.exports = {
|
|||
},
|
||||
transitionTimingFunction: {
|
||||
ease: 'ease',
|
||||
linear: 'linear',
|
||||
},
|
||||
// TODO: Backport to GitLab UI.
|
||||
borderRadius: {
|
||||
|
|
@ -267,8 +268,16 @@ module.exports = {
|
|||
'1/2': '50%',
|
||||
},
|
||||
transitionProperty: {
|
||||
transform: 'transform',
|
||||
background: 'background',
|
||||
opacity: 'opacity',
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
width: 'width',
|
||||
stroke: 'stroke',
|
||||
padding: 'padding',
|
||||
'stroke-opacity': 'stroke-opacity',
|
||||
'box-shadow': 'box-shadow',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
DEFAULT: 'ease',
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147509
|
|||
milestone: '17.1'
|
||||
queued_migration_version: 20240522183910
|
||||
finalize_after: '2024-06-20'
|
||||
finalized_by:
|
||||
finalized_by: 20240701182755
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
table_name: import_source_user_placeholder_references
|
||||
classes:
|
||||
- Import::SourceUserPlaceholderReference
|
||||
feature_categories:
|
||||
- importers
|
||||
description: Used to map placeholder user references from imported data to real users
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156241
|
||||
milestone: '17.2'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
allow_cross_foreign_keys:
|
||||
- gitlab_main_clusterwide
|
||||
sharding_key:
|
||||
namespace_id: namespaces
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateImportSourceUserPlaceholderReference < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.2'
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
INDEX_NAME = 'index_import_source_user_placeholder_references_on_source_user_'
|
||||
|
||||
def change
|
||||
create_table :import_source_user_placeholder_references do |t|
|
||||
t.references :source_user,
|
||||
index: { name: INDEX_NAME },
|
||||
null: false,
|
||||
foreign_key: { to_table: :import_source_users, on_delete: :cascade }
|
||||
t.references :namespace, null: false, index: true, foreign_key: { on_delete: :cascade }
|
||||
t.bigint :numeric_key, null: true
|
||||
t.datetime_with_timezone :created_at, null: false
|
||||
t.text :model, limit: 150, null: false
|
||||
t.text :user_reference_column, limit: 50, null: false
|
||||
t.jsonb :composite_key, null: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeBackfillEpicIssuesIntoWorkItemParentLinks < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.2'
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillEpicIssuesIntoWorkItemParentLinks',
|
||||
table_name: :epic_issues,
|
||||
column_name: 'id',
|
||||
job_arguments: [nil],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# No op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
91e467973c28e98ed562c70ae108f7b5cb1ed0353e3ce0c8b13f052077c75d5a
|
||||
|
|
@ -0,0 +1 @@
|
|||
154e67c707fec8122c216af36d26919cc4317089506719ead7a2f2d203a16160
|
||||
|
|
@ -11167,6 +11167,28 @@ CREATE SEQUENCE import_failures_id_seq
|
|||
|
||||
ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id;
|
||||
|
||||
CREATE TABLE import_source_user_placeholder_references (
|
||||
id bigint NOT NULL,
|
||||
source_user_id bigint NOT NULL,
|
||||
namespace_id bigint NOT NULL,
|
||||
numeric_key bigint,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
model text NOT NULL,
|
||||
user_reference_column text NOT NULL,
|
||||
composite_key jsonb,
|
||||
CONSTRAINT check_782140eb9d CHECK ((char_length(user_reference_column) <= 50)),
|
||||
CONSTRAINT check_d17bd9dd4d CHECK ((char_length(model) <= 150))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE import_source_user_placeholder_references_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE import_source_user_placeholder_references_id_seq OWNED BY import_source_user_placeholder_references.id;
|
||||
|
||||
CREATE TABLE import_source_users (
|
||||
id bigint NOT NULL,
|
||||
placeholder_user_id bigint,
|
||||
|
|
@ -20801,6 +20823,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo
|
|||
|
||||
ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY import_source_user_placeholder_references ALTER COLUMN id SET DEFAULT nextval('import_source_user_placeholder_references_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY import_source_users ALTER COLUMN id SET DEFAULT nextval('import_source_users_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_policies_id_seq'::regclass);
|
||||
|
|
@ -22987,6 +23011,9 @@ ALTER TABLE ONLY import_export_uploads
|
|||
ALTER TABLE ONLY import_failures
|
||||
ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY import_source_user_placeholder_references
|
||||
ADD CONSTRAINT import_source_user_placeholder_references_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY import_source_users
|
||||
ADD CONSTRAINT import_source_users_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -27322,6 +27349,10 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI
|
|||
|
||||
CREATE INDEX index_import_failures_on_user_id_not_null ON import_failures USING btree (user_id) WHERE (user_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON import_source_user_placeholder_references USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_import_source_user_placeholder_references_on_source_user_ ON import_source_user_placeholder_references USING btree (source_user_id);
|
||||
|
||||
CREATE INDEX index_import_source_users_on_namespace_id ON import_source_users USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
|
||||
|
|
@ -33352,6 +33383,9 @@ ALTER TABLE ONLY namespaces_storage_limit_exclusions
|
|||
ALTER TABLE ONLY users_security_dashboard_projects
|
||||
ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY import_source_user_placeholder_references
|
||||
ADD CONSTRAINT fk_rails_158995b934 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY import_source_users
|
||||
ADD CONSTRAINT fk_rails_167f82fd95 FOREIGN KEY (reassign_to_user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
|
|
@ -34720,6 +34754,9 @@ ALTER TABLE ONLY upload_states
|
|||
ALTER TABLE ONLY epic_metrics
|
||||
ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY import_source_user_placeholder_references
|
||||
ADD CONSTRAINT fk_rails_d0b75c434e FOREIGN KEY (source_user_id) REFERENCES import_source_users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY subscriptions
|
||||
ADD CONSTRAINT fk_rails_d0c8bda804 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ To find out more about reporting abuse, see
|
|||
To access abuse reports:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin Area**.
|
||||
1. Select **Abuse Reports**.
|
||||
1. Select **Abuse reports**.
|
||||
|
||||
There are four ways to resolve an abuse report, with a button for each method:
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ There are four ways to resolve an abuse report, with a button for each method:
|
|||
- Allows the user to create issues, notes, snippets, and merge requests without being blocked for spam.
|
||||
- Prevents abuse reports from being created for this user.
|
||||
|
||||
The following is an example of the **Abuse Reports** page:
|
||||
The following is an example of the **Abuse reports** page:
|
||||
|
||||

|
||||
|
||||
|
|
@ -80,7 +80,7 @@ After blocking, you can still either:
|
|||
- Remove the user and report if necessary.
|
||||
- Remove the report.
|
||||
|
||||
The following is an example of a blocked user listed on the **Abuse Reports**
|
||||
The following is an example of a blocked user listed on the **Abuse reports**
|
||||
page:
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ To run the QA Evaluation test locally, the following environment variables
|
|||
must be exported:
|
||||
|
||||
```ruby
|
||||
REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb
|
||||
ANTHROPIC_API_KEY='your-key' VERTEX_AI_PROJECT='your-project-id' REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb
|
||||
```
|
||||
|
||||
## Testing with CI
|
||||
|
|
|
|||
|
|
@ -110,14 +110,10 @@ It respects a rate limit of 450 embeddings per minute. Reach out to `@maddievn`
|
|||
|
||||
If the following returns 0, all issues for the project have embeddings:
|
||||
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
```shell
|
||||
curl "http://localhost:9200/gitlab-development-issues/_count" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"query": {"bool": {"filter": [{"term": {"project_id": PROJECT_ID}}], "must_not": [{"exists": {"field": "embedding"}}]}}}' | jq '.count'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Replacing `PROJECT_ID` with your project ID.
|
||||
|
|
|
|||
|
|
@ -46,12 +46,15 @@ Continuous Vulnerability Scanning supports components with the following [PURL t
|
|||
- `npm`
|
||||
- `nuget`
|
||||
- `pypi`
|
||||
- `rpm`
|
||||
|
||||
Work to support `apk` and `rpm` package URL types is tracked in [issue 428703](https://gitlab.com/gitlab-org/gitlab/-/issues/428703).
|
||||
Work to support the `apk` package URL type is tracked in [issue 428703](https://gitlab.com/gitlab-org/gitlab/-/issues/428703).
|
||||
|
||||
Go pseudo versions are not supported. A project dependency that references a Go pseudo version is
|
||||
never considered as affected because this might result in false negatives.
|
||||
|
||||
RPM versions containing `^` are not supported. Work to support these versions is tracked in [issue 459969](https://gitlab.com/gitlab-org/gitlab/-/issues/459969).
|
||||
|
||||
## How to generate a CycloneDX SBOM report
|
||||
|
||||
GitLab offers security analyzers that can generate a [CycloneDX SBOM report](../../../ci/yaml/artifacts_reports.md#artifactsreportscyclonedx) compatible with GitLab:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ The following policy types are available:
|
|||
pipeline or on a specified schedule.
|
||||
- [Merge request approval policy](scan-result-policies.md). Enforce project-level settings and
|
||||
approval rules based on scan results.
|
||||
- [Pipeline execution policy](pipeline_execution_policies.md). Enforce CI/CD jobs as part of project pipelines.
|
||||
|
||||
## Security policy project
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
stage: Govern
|
||||
group: Security Policies
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Pipeline execution policies
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/13266) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `pipeline_execution_policy_type`. Disabled by default.
|
||||
|
||||
Use Pipeline execution policies to enforce CI/CD jobs for all applicable projects.
|
||||
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For a video walkthrough, see [Security Policies: Pipeline Execution Policy Type](https://www.youtube.com/watch?v=QQAOpkZ__pA).
|
||||
|
||||
## Pipeline execution policies schema
|
||||
|
||||
The YAML file with pipeline execution policies consists of an array of objects matching pipeline execution
|
||||
policy schema nested under the `pipeline_execution_policy` key. You can configure a maximum of five
|
||||
policies under the `pipeline_execution_policy` key. Any other policies configured after
|
||||
the first five are not applied.
|
||||
|
||||
When you save a new policy, GitLab validates its contents against [this JSON schema](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/validators/json_schemas/security_orchestration_policy.json).
|
||||
If you're not familiar with how to read [JSON schemas](https://json-schema.org/),
|
||||
the following sections and tables provide an alternative.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `pipeline_execution_policy` | `array` of pipeline execution policy | true | List of pipeline execution policies (maximum five) |
|
||||
|
||||
## Pipeline execution policy schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | `string` | true | Name of the policy. Maximum of 255 characters.|
|
||||
| `description` (optional) | `string` | true | Description of the policy. |
|
||||
| `enabled` | `boolean` | true | Flag to enable (`true`) or disable (`false`) the policy. |
|
||||
| `content` | `object` of [`content`](#content-type) | true | Reference to the CI/CD configuration to inject into project pipelines. |
|
||||
| `pipeline_config_strategy` | `string` | false | Can either be `inject_ci` or `override_project_ci`. Defines the method for merging the policy configuration with the project pipeline. `inject_ci` preserves the project CI configuration and injects additional jobs from the policy. Having multiple policies enabled injects all jobs additively. `override_project_ci` replaces the project CI configuration and keeps only the policy jobs in the pipeline. |
|
||||
| `policy_scope` | `object` of [`policy_scope`](#policy_scope-scope-type) | false | Scopes the policy based on compliance framework labels or projects you define. |
|
||||
|
||||
Note the following:
|
||||
|
||||
- Jobs variables from pipeline execution policies take precedence over the project's CI/CD configuration.
|
||||
- Users triggering a pipeline must have at least read access to CI file specified in `content`.
|
||||
- Pipeline execution policy jobs can be assigned to one of the two reserved stages:
|
||||
- `.pipeline-policy-pre` at the beginning of the pipeline, before the `.pre` stage.
|
||||
- `.pipeline-policy-post` at the very end of the pipeline, after the .post stage.
|
||||
- Injecting jobs in any of the reserved stages is guaranteed to always work. Execution policy jobs can also be assigned to any standard (build, test, deploy) or user-declared stages. However, in this case, the jobs may be ignored depending on the project pipeline configuration.
|
||||
- It is not possible to assign jobs to reserved stages outside of a pipeline execution policy.
|
||||
|
||||
### `content` type
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `project` | `string` | true | The full GitLab project path to a project on the same GitLab instance. |
|
||||
| `file` | `string` | true | A full file path relative to the root directory (/). The YAML files must have the `.yml` or `.yaml` extension. |
|
||||
| `ref` | `string` | false | The ref to retrieve the file from. Defaults to the HEAD of the project when not specified. |
|
||||
|
||||
### `policy_scope` scope type
|
||||
|
||||
| Field | Type | Possible values | Description |
|
||||
|-------|------|-----------------|-------------|
|
||||
| `compliance_frameworks` | `array` | | List of IDs of the compliance frameworks in scope of enforcement, in an array of objects with key `id`. |
|
||||
| `projects` | `object` | `including`, `excluding` | Use `excluding:` or `including:` then list the IDs of the projects you wish to include or exclude, in an array of objects with key `id`. |
|
||||
|
||||
### Example security policies project
|
||||
|
||||
You can use the following example in a `.gitlab/security-policies/policy.yml` file stored in a
|
||||
[security policy project](index.md#security-policy-project):
|
||||
|
||||
```yaml
|
||||
---
|
||||
pipeline_execution_policy:
|
||||
- name: My pipeline execution policy
|
||||
description: Enforces CI/CD jobs
|
||||
enabled: true
|
||||
pipeline_config_strategy: override_project_ci
|
||||
content:
|
||||
include:
|
||||
- project: verify-issue-469027/policy-ci
|
||||
file: policy-ci.yml
|
||||
ref: main # optional
|
||||
policy_scope:
|
||||
projects:
|
||||
including:
|
||||
- id: 361
|
||||
```
|
||||
|
|
@ -48,8 +48,6 @@ module API
|
|||
end
|
||||
get ':id/repository/branches', urgency: :low do
|
||||
cache_action([user_project, :branches, current_user, declared_params], expires_in: 30.seconds) do
|
||||
user_project.preload_protected_branches
|
||||
|
||||
repository = user_project.repository
|
||||
|
||||
branches_finder = BranchesFinder.new(repository, declared_params(include_missing: false))
|
||||
|
|
@ -57,6 +55,7 @@ module API
|
|||
|
||||
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
|
||||
|
||||
user_project.preload_protected_branches if branches.present?
|
||||
present_cached(
|
||||
branches,
|
||||
with: Entities::Branch,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ module API
|
|||
type: 'boolean',
|
||||
example: true
|
||||
} do |repo_branch, options|
|
||||
::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
|
||||
::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].all_protected_branches)
|
||||
end
|
||||
|
||||
expose :developers_can_merge,
|
||||
|
|
@ -44,7 +44,7 @@ module API
|
|||
type: 'boolean',
|
||||
example: true
|
||||
} do |repo_branch, options|
|
||||
::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
|
||||
::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].all_protected_branches)
|
||||
end
|
||||
|
||||
expose :can_push,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def apply_headers(records, next_cursor)
|
||||
if records.count == params[:per_page]
|
||||
if records.count == params[:per_page] && next_cursor.present?
|
||||
Gitlab::Pagination::Keyset::HeaderBuilder
|
||||
.new(request_context)
|
||||
.add_next_page_header(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Sidebars
|
|||
|
||||
override :title
|
||||
def title
|
||||
s_('Admin|Abuse Reports')
|
||||
s_('Admin|Abuse reports')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
|
|
|
|||
|
|
@ -2421,9 +2421,6 @@ msgstr ""
|
|||
msgid "About your company"
|
||||
msgstr ""
|
||||
|
||||
msgid "Abuse Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Abuse reports"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4650,7 +4647,7 @@ msgstr ""
|
|||
msgid "Admin|AI-Powered Features"
|
||||
msgstr ""
|
||||
|
||||
msgid "Admin|Abuse Reports"
|
||||
msgid "Admin|Abuse reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Admin|Additional users must be reviewed and approved by a system administrator. Learn more about %{help_link_start}usage caps%{help_link_end}."
|
||||
|
|
@ -36152,6 +36149,9 @@ msgstr[1] ""
|
|||
msgid "On the left sidebar, select %{compliance_center_link} to view them."
|
||||
msgstr ""
|
||||
|
||||
msgid "On this page"
|
||||
msgstr ""
|
||||
|
||||
msgid "On track"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :import_source_user_placeholder_reference, class: 'Import::SourceUserPlaceholderReference' do
|
||||
source_user factory: :import_source_user
|
||||
namespace
|
||||
model { 'Note' }
|
||||
user_reference_column { 'author_id' }
|
||||
numeric_key { 1 }
|
||||
end
|
||||
end
|
||||
|
|
@ -29,7 +29,7 @@ RSpec.describe "Gitlab::Experiment", :js, feature_category: :activation do
|
|||
|
||||
visit admin_abuse_reports_path
|
||||
|
||||
expect(page).to have_content('Abuse Reports')
|
||||
expect(page).to have_content('Abuse reports')
|
||||
|
||||
published_experiments = page.evaluate_script('window.gl.experiments')
|
||||
expect(published_experiments).to include({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import Heading from '~/content_editor/extensions/heading';
|
||||
import { toTree, getHeadings, fillEmpty } from '~/content_editor/services/table_of_contents_utils';
|
||||
import {
|
||||
toTree,
|
||||
getHeadings,
|
||||
fillEmpty,
|
||||
getHeadingsFromDOM,
|
||||
} from '~/content_editor/services/table_of_contents_utils';
|
||||
import { createTestEditor, createDocBuilder } from '../test_utils';
|
||||
|
||||
describe('content_editor/services/table_of_content_utils', () => {
|
||||
|
|
@ -144,4 +149,57 @@ describe('content_editor/services/table_of_content_utils', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHeadingsFromDOM', () => {
|
||||
it('gets all headings as a tree in a DOM element', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = `
|
||||
<h1><a href="#heading-1"></a>Heading 1</h1>
|
||||
<h2><a href="#heading-1-1"></a>Heading 1.1</h2>
|
||||
<h3><a href="#heading-1-1-1"></a>Heading 1.1.1</h3>
|
||||
<h2><a href="#heading-1-2"></a>Heading 1.2</h2>
|
||||
<h3><a href="#heading-1-2-1"></a>Heading 1.2.1</h3>
|
||||
<h2><a href="#heading-1-3"></a>Heading 1.3</h2>
|
||||
<h2><a href="#heading-1-4"></a>Heading 1.4</h2>
|
||||
<h3><a href="#heading-1-4-1"></a>Heading 1.4.1</h3>
|
||||
<h1><a href="#heading-2"></a>Heading 2</h1>
|
||||
`;
|
||||
|
||||
expect(getHeadingsFromDOM(element)).toEqual([
|
||||
{
|
||||
href: '#heading-1',
|
||||
level: 1,
|
||||
subHeadings: [
|
||||
{
|
||||
href: '#heading-1-1',
|
||||
level: 2,
|
||||
subHeadings: [
|
||||
{ href: '#heading-1-1-1', level: 3, subHeadings: [], text: 'Heading 1.1.1' },
|
||||
],
|
||||
text: 'Heading 1.1',
|
||||
},
|
||||
{
|
||||
href: '#heading-1-2',
|
||||
level: 2,
|
||||
subHeadings: [
|
||||
{ href: '#heading-1-2-1', level: 3, subHeadings: [], text: 'Heading 1.2.1' },
|
||||
],
|
||||
text: 'Heading 1.2',
|
||||
},
|
||||
{ href: '#heading-1-3', level: 2, subHeadings: [], text: 'Heading 1.3' },
|
||||
{
|
||||
href: '#heading-1-4',
|
||||
level: 2,
|
||||
subHeadings: [
|
||||
{ href: '#heading-1-4-1', level: 3, subHeadings: [], text: 'Heading 1.4.1' },
|
||||
],
|
||||
text: 'Heading 1.4',
|
||||
},
|
||||
],
|
||||
text: 'Heading 1',
|
||||
},
|
||||
{ href: '#heading-2', level: 1, subHeadings: [], text: 'Heading 2' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import { DISCUSSION_NOTE, ASC, DESC } from '~/notes/constants';
|
||||
import mutations from '~/notes/stores/mutations';
|
||||
import {
|
||||
|
|
@ -598,13 +597,15 @@ describe('Notes Store mutations', () => {
|
|||
});
|
||||
|
||||
it('keeps reactivity of discussion', () => {
|
||||
const state = {};
|
||||
Vue.set(state, 'discussions', [
|
||||
{
|
||||
id: 1,
|
||||
expanded: false,
|
||||
},
|
||||
]);
|
||||
const state = {
|
||||
discussions: [
|
||||
{
|
||||
id: 1,
|
||||
expanded: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const discussion = state.discussions[0];
|
||||
|
||||
mutations.SET_DISCUSSION_DIFF_LINES(state, {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
|
|||
it('calls renderGFM after nextTick', async () => {
|
||||
await nextTick();
|
||||
|
||||
expect(renderGFM).toHaveBeenCalledWith(wrapper.element);
|
||||
expect(renderGFM).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles hash after render', async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
|
||||
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
|
||||
import { childrenWorkItems } from '../../mock_data';
|
||||
|
||||
describe('WorkItemTreeChildren', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ children = childrenWorkItems } = {}) => {
|
||||
wrapper = shallowMount(WorkItemTreeChildren, {
|
||||
propsData: {
|
||||
workItemType: 'Objective',
|
||||
workItemId: 'gid:://gitlab/WorkItem/1',
|
||||
children,
|
||||
canUpdate: true,
|
||||
showLabels: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
|
||||
const findWorkItemLinkChildItem = () => findWorkItemLinkChildItems().at(0);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders all WorkItemLinkChildItems', () => {
|
||||
expect(findWorkItemLinkChildItems().length).toBe(4);
|
||||
});
|
||||
|
||||
it('emits childItem from WorkItemLinkChildItems on `click` event', () => {
|
||||
const event = {
|
||||
childItem: 'gid://gitlab/WorkItem/2',
|
||||
};
|
||||
|
||||
findWorkItemLinkChildItem().vm.$emit('click', event);
|
||||
|
||||
expect(wrapper.emitted('click')).toEqual([[{ childItem: 'gid://gitlab/WorkItem/2' }]]);
|
||||
});
|
||||
|
||||
it('emits immediate childItem on `click` event', () => {
|
||||
const event = expect.anything();
|
||||
|
||||
findWorkItemLinkChildItem().vm.$emit('click', event);
|
||||
|
||||
expect(wrapper.emitted('click')).toEqual([[{ childItem: 'gid://gitlab/WorkItem/2' }]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -645,6 +645,7 @@ project:
|
|||
- notes
|
||||
- snippets
|
||||
- hooks
|
||||
- all_protected_branches
|
||||
- protected_branches
|
||||
- protected_tags
|
||||
- project_members
|
||||
|
|
|
|||
|
|
@ -129,6 +129,18 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager, feature_category: :source_
|
|||
pager.paginate(finder)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current page includes all requested elements and cursor is empty' do
|
||||
let(:base_query) { { per_page: 2 } }
|
||||
let(:branches) { [branch1, branch2] }
|
||||
let(:next_cursor) { '' }
|
||||
|
||||
it 'uses keyset pagination without link headers' do
|
||||
expect(request_context).not_to receive(:header).with('Link', anything)
|
||||
|
||||
pager.paginate(finder)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Sidebars::Admin::Menus::AbuseReportsMenu, feature_category: :navigation do
|
||||
it_behaves_like 'Admin menu',
|
||||
link: '/admin/abuse_reports',
|
||||
title: _('Abuse Reports'),
|
||||
title: _('Abuse reports'),
|
||||
icon: 'slight-frown'
|
||||
|
||||
it_behaves_like 'Admin menu without sub menus', active_routes: { controller: :abuse_reports }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FinalizeBackfillEpicIssuesIntoWorkItemParentLinks, feature_category: :database, migration_version: 20240701182755 do
|
||||
describe '#up' do
|
||||
it 'ensures the migration is completed for self-managed instances' do
|
||||
# enqueue the migration
|
||||
QueueBackfillEpicIssuesIntoWorkItemParentLinks.new.up
|
||||
|
||||
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.where(
|
||||
job_class_name: 'BackfillEpicIssuesIntoWorkItemParentLinks',
|
||||
table_name: 'epic_issues'
|
||||
).first
|
||||
|
||||
expect(migration.status).not_to eq(6) # finalized
|
||||
|
||||
migrate!
|
||||
|
||||
expect(migration.reload.status).to eq(6)
|
||||
QueueBackfillEpicIssuesIntoWorkItemParentLinks.new.down
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :importers do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:source_user).class_name('Import::SourceUser') }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:user_reference_column) }
|
||||
it { is_expected.to validate_presence_of(:model) }
|
||||
it { is_expected.to validate_presence_of(:namespace_id) }
|
||||
it { is_expected.to validate_presence_of(:source_user_id) }
|
||||
it { is_expected.to validate_numericality_of(:numeric_key).only_integer.is_greater_than(0) }
|
||||
it { expect(described_class).to validate_jsonb_schema(['composite_key']) }
|
||||
it { is_expected.to allow_value({ id: 1 }).for(:composite_key) }
|
||||
it { is_expected.to allow_value({ id: '1' }).for(:composite_key) }
|
||||
it { is_expected.to allow_value({ foo: '1', bar: 2 }).for(:composite_key) }
|
||||
it { is_expected.not_to allow_value({}).for(:composite_key) }
|
||||
it { is_expected.not_to allow_value({ id: 'foo' }).for(:composite_key) }
|
||||
it { is_expected.not_to allow_value(1).for(:composite_key) }
|
||||
|
||||
describe '#validate_numeric_or_composite_key_present' do
|
||||
def validation_errors(...)
|
||||
described_class.new(...).tap(&:validate)
|
||||
.errors
|
||||
.where(:base)
|
||||
end
|
||||
|
||||
it 'must have numeric_key or composite_key present', :aggregate_failures do
|
||||
expect(validation_errors).to be_present
|
||||
expect(validation_errors(numeric_key: 1)).to be_blank
|
||||
expect(validation_errors(composite_key: { id: 1 })).to be_blank
|
||||
expect(validation_errors(numeric_key: 1, composite_key: { id: 1 })).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'is destroyed when source user is destroyed' do
|
||||
reference = create(:import_source_user_placeholder_reference)
|
||||
|
||||
expect { reference.source_user.destroy! }.to change { described_class.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
|
@ -196,6 +196,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
it { is_expected.to have_many(:alert_hooks_integrations).class_name('Integration') }
|
||||
it { is_expected.to have_many(:incident_hooks_integrations).class_name('Integration') }
|
||||
it { is_expected.to have_many(:relation_import_trackers).class_name('Projects::ImportExport::RelationImportTracker') }
|
||||
it { is_expected.to have_many(:all_protected_branches).class_name('ProtectedBranch') }
|
||||
|
||||
# GitLab Pages
|
||||
it { is_expected.to have_many(:pages_domains) }
|
||||
|
|
|
|||
|
|
@ -83,34 +83,14 @@ RSpec.describe ProtectedBranch, feature_category: :source_code_management do
|
|||
end
|
||||
|
||||
describe '.protected_refs' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let(:project) { build_stubbed(:project) }
|
||||
|
||||
subject { described_class.protected_refs(project) }
|
||||
|
||||
context 'when feature flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(group_protected_branches: true)
|
||||
stub_feature_flags(allow_protected_branches_for_group: true)
|
||||
end
|
||||
it 'call `all_protected_branches`' do
|
||||
expect(project).to receive(:all_protected_branches)
|
||||
|
||||
it 'call `all_protected_branches`' do
|
||||
expect(project).to receive(:all_protected_branches)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_protected_branches: false)
|
||||
stub_feature_flags(allow_protected_branches_for_group: false)
|
||||
end
|
||||
|
||||
it 'call `protected_branches`' do
|
||||
expect(project).to receive(:protected_branches)
|
||||
|
||||
subject
|
||||
end
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe API::Branches, feature_category: :source_code_management do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:project) { create(:project, :repository, creator: user, path: 'my.project', create_branch: 'ends-with.txt') }
|
||||
let(:project) { create(:project, :in_group, :repository, creator: user, path: 'my.project', create_branch: 'ends-with.txt') }
|
||||
let(:guest) { create(:user, guest_of: project) }
|
||||
let(:branch_name) { 'feature' }
|
||||
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
|
||||
|
|
@ -295,10 +295,79 @@ RSpec.describe API::Branches, feature_category: :source_code_management do
|
|||
new_branch_name = 'protected-branch'
|
||||
::Branches::CreateService.new(project, current_user).execute(new_branch_name, 'master')
|
||||
create(:protected_branch, name: new_branch_name, project: project)
|
||||
create(:protected_branch, name: new_branch_name, project: nil, group: project.group)
|
||||
|
||||
expect do
|
||||
get api(route, current_user), params: { per_page: 100 }
|
||||
end.not_to exceed_query_limit(control).with_threshold(1)
|
||||
end.not_to exceed_query_limit(control)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with group protected branches' do
|
||||
subject(:request) { get api(route, user) }
|
||||
|
||||
let!(:group_protected_branch) do
|
||||
create(:protected_branch,
|
||||
*access_levels,
|
||||
project: nil,
|
||||
group: project.group,
|
||||
name: '*'
|
||||
)
|
||||
end
|
||||
|
||||
context 'maintainers allowed to push and merge' do
|
||||
let(:access_levels) { [] }
|
||||
|
||||
it 'responds with correct attributes related to push and merge' do
|
||||
request
|
||||
|
||||
expect(json_response.dig(0, 'developers_can_merge')).to be_falsey
|
||||
expect(json_response.dig(0, 'developers_can_push')).to be_falsey
|
||||
expect(json_response.dig(0, 'can_push')).to be_truthy
|
||||
end
|
||||
|
||||
context 'and there is a more permissive project level protected branch' do
|
||||
let!(:project_level_protected_branch) do
|
||||
create(:protected_branch,
|
||||
:developers_can_merge,
|
||||
:developers_can_push,
|
||||
project: project,
|
||||
name: '*'
|
||||
)
|
||||
end
|
||||
|
||||
it 'responds with correct attributes related to push and merge' do
|
||||
request
|
||||
|
||||
expect(json_response.dig(0, 'developers_can_merge')).to be_truthy
|
||||
expect(json_response.dig(0, 'developers_can_push')).to be_truthy
|
||||
expect(json_response.dig(0, 'can_push')).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when developers can push and merge' do
|
||||
let(:access_levels) { %i[developers_can_merge developers_can_push] }
|
||||
|
||||
it 'responds with correct attributes related to push and merge' do
|
||||
request
|
||||
|
||||
expect(json_response.dig(0, 'developers_can_merge')).to be_truthy
|
||||
expect(json_response.dig(0, 'developers_can_push')).to be_truthy
|
||||
expect(json_response.dig(0, 'can_push')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no one can push and merge' do
|
||||
let(:access_levels) { %i[no_one_can_merge no_one_can_push] }
|
||||
|
||||
it 'responds with correct attributes related to push and merge' do
|
||||
request
|
||||
|
||||
expect(json_response.dig(0, 'developers_can_merge')).to be_falsey
|
||||
expect(json_response.dig(0, 'developers_can_push')).to be_falsey
|
||||
expect(json_response.dig(0, 'can_push')).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,25 @@ RSpec.shared_examples 'User views a wiki page' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a page has headings' do
|
||||
before do
|
||||
wiki_page.update(content: "# Heading 1\n\n## Heading 1.1\n\n### Heading 1.1.1\n\n# Heading 2") # rubocop:disable Rails/SaveBang -- not an ActiveRecord
|
||||
end
|
||||
|
||||
it 'displays the table of contents for the page' do
|
||||
visit(wiki_page_path(wiki, wiki_page))
|
||||
|
||||
within '.js-wiki-toc' do
|
||||
expect(page).to have_content('On this page')
|
||||
|
||||
expect(page).to have_content('Heading 1')
|
||||
expect(page).to have_content('Heading 1.1')
|
||||
expect(page).to have_content('Heading 1.1.1')
|
||||
expect(page).to have_content('Heading 2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when page has invalid content encoding' do
|
||||
let(:content) { (+'whatever').force_encoding('ISO-8859-1') }
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ func webSocketHandler(upgrader *websocket.Upgrader, connCh chan connWithReq) htt
|
|||
func channelOkBody(remote *httptest.Server, header http.Header, subprotocols ...string) *api.Response {
|
||||
out := &api.Response{
|
||||
Channel: &api.ChannelSettings{
|
||||
WsURL: websocketURL(remote.URL),
|
||||
Url: websocketURL(remote.URL),
|
||||
Header: header,
|
||||
Subprotocols: subprotocols,
|
||||
MaxSessionTime: 0,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ type ChannelSettings struct {
|
|||
Subprotocols []string
|
||||
|
||||
// The websocket URL to connect to.
|
||||
WsURL string
|
||||
Url string //nolint:revive,stylecheck // when JSON decoding, the value is provided via 'url'
|
||||
|
||||
// Any headers (e.g., Authorization) to send with the websocket request
|
||||
Header http.Header
|
||||
|
|
@ -35,7 +35,7 @@ type ChannelSettings struct {
|
|||
|
||||
// URL parses the websocket URL in the ChannelSettings and returns a *url.URL.
|
||||
func (t *ChannelSettings) URL() (*url.URL, error) {
|
||||
return url.Parse(t.WsURL)
|
||||
return url.Parse(t.Url)
|
||||
}
|
||||
|
||||
// Dialer returns a websocket Dialer configured with the settings from ChannelSettings.
|
||||
|
|
@ -72,7 +72,7 @@ func (t *ChannelSettings) Clone() *ChannelSettings {
|
|||
// Dial establishes a websocket connection using the settings from ChannelSettings.
|
||||
// It returns a websocket connection, an HTTP response, and an error if any.
|
||||
func (t *ChannelSettings) Dial() (*websocket.Conn, *http.Response, error) {
|
||||
return t.Dialer().Dial(t.WsURL, t.Header)
|
||||
return t.Dialer().Dial(t.Url, t.Header)
|
||||
}
|
||||
|
||||
// Validate checks if the ChannelSettings instance is valid.
|
||||
|
|
@ -133,7 +133,7 @@ func (t *ChannelSettings) IsEqual(other *ChannelSettings) bool {
|
|||
}
|
||||
}
|
||||
|
||||
return t.WsURL == other.WsURL &&
|
||||
return t.Url == other.Url &&
|
||||
t.CAPem == other.CAPem &&
|
||||
t.MaxSessionTime == other.MaxSessionTime
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func channel(url string, subprotocols ...string) *ChannelSettings {
|
||||
return &ChannelSettings{
|
||||
WsURL: url,
|
||||
Url: url,
|
||||
Subprotocols: subprotocols,
|
||||
MaxSessionTime: 0,
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ func TestIsEqual(t *testing.T) {
|
|||
{chann, chann, true},
|
||||
{chann.Clone(), chann.Clone(), true},
|
||||
{chann, channel("foo:"), false},
|
||||
{chann, channel(chann.WsURL), false},
|
||||
{chann, channel(chann.Url), false},
|
||||
{header(chann), header(chann), true},
|
||||
{channHeader2, channHeader2, true},
|
||||
{channHeader3, channHeader3, true},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func checkerSeries(values ...*api.ChannelSettings) AuthCheckerFunc {
|
|||
}
|
||||
|
||||
func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
|
||||
template := &api.ChannelSettings{WsURL: "ws://example.com"}
|
||||
template := &api.ChannelSettings{Url: "ws://example.com"}
|
||||
stopCh := make(chan error)
|
||||
series := checkerSeries(template, template, template)
|
||||
ac := NewAuthChecker(series, template, stopCh)
|
||||
|
|
@ -35,9 +35,9 @@ func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAuthCheckerStopsWhenAuthChanges(t *testing.T) {
|
||||
template := &api.ChannelSettings{WsURL: "ws://example.com"}
|
||||
template := &api.ChannelSettings{Url: "ws://example.com"}
|
||||
changed := template.Clone()
|
||||
changed.WsURL = "wss://example.com"
|
||||
changed.Url = "wss://example.com"
|
||||
stopCh := make(chan error)
|
||||
series := checkerSeries(template, changed, template)
|
||||
ac := NewAuthChecker(series, template, stopCh)
|
||||
|
|
|
|||
Loading…
Reference in New Issue