Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-02 15:16:59 +00:00
parent dfb41a436c
commit 5ff17740d4
66 changed files with 732 additions and 129 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@
}
.sidebar-container {
padding: 20px 0;
padding-right: 100px;
height: 100%;
overflow-y: scroll;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
91e467973c28e98ed562c70ae108f7b5cb1ed0353e3ce0c8b13f052077c75d5a

View File

@ -0,0 +1 @@
154e67c707fec8122c216af36d26919cc4317089506719ead7a2f2d203a16160

View File

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

View File

@ -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:
![abuse-reports-page-image](img/abuse_reports_page_v13_11.png)
@ -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:
![abuse-report-blocked-user-image](img/abuse_report_blocked_user.png)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ module Sidebars
override :title
def title
s_('Admin|Abuse Reports')
s_('Admin|Abuse reports')
end
override :sprite_icon

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -645,6 +645,7 @@ project:
- notes
- snippets
- hooks
- all_protected_branches
- protected_branches
- protected_tags
- project_members

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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